refactor: collapse GATEWAY_USER_ID into IRONCLAW_OWNER_ID

Remove the separate GATEWAY_USER_ID config. The gateway now uses
IRONCLAW_OWNER_ID (config.owner_id) directly for auth identity,
bootstrap user creation, and workspace scoping.

Previously, with_owner_scope() rebinds the auth identity to owner_id
while keeping default_sender_id as the gateway user_id. This caused
a FK constraint violation when creating users because the auth
identity ("default") didn't match any user in the DB ("nearai").

Changes:
- Remove GATEWAY_USER_ID env var and gateway_user_id from settings
- Remove user_id field from GatewayConfig
- Add owner_id parameter to GatewayChannel::new()
- Remove with_owner_scope() method
- Remove default_sender_id from GatewayState
- Remove sender override logic in chat/approval handlers
- Remove debug endpoint and tracing from prior debugging
- Update all tests and E2E fixtures

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
This commit is contained in:
Illia Polosukhin
2026-03-26 11:19:45 -07:00
co-authored by Claude Opus 4.6
parent 6cc77fe162
commit 82a9df1ebf
16 changed files with 15 additions and 160 deletions
+2 -13
View File
@@ -48,7 +48,6 @@ pub async fn users_create_handler(
));
}
tracing::info!("users_create: passed validation, building record");
let user_id = Uuid::new_v4().to_string();
let now = chrono::Utc::now();
@@ -68,15 +67,10 @@ pub async fn users_create_handler(
metadata: serde_json::json!({}),
};
tracing::info!("users_create: calling create_user for {}", user_id);
store
.create_user(&user_record)
.await
.map_err(|e| {
tracing::error!("users_create: create_user failed: {e}");
(StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
})?;
tracing::info!("users_create: create_user succeeded");
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
// Generate a first API token so the new user can authenticate immediately.
// Hash the hex-encoded plaintext (what the user sends as Bearer token),
@@ -87,15 +81,10 @@ pub async fn users_create_handler(
let token_hash = crate::channels::web::auth::hash_token(&plaintext_token);
let token_prefix = &plaintext_token[..8];
tracing::info!("users_create: calling create_api_token");
let _token_record = store
.create_api_token(&user_id, "initial", &token_hash, token_prefix, None)
.await
.map_err(|e| {
tracing::error!("users_create: create_api_token failed: {e}");
(StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
})?;
tracing::info!("users_create: complete, returning response");
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
Ok(Json(serde_json::json!({
"id": user_record.id,
+3 -17
View File
@@ -73,7 +73,7 @@ impl GatewayChannel {
///
/// If no auth token is configured, generates a random one and prints it.
/// Builds a single-user `MultiAuthState` from the config.
pub fn new(config: GatewayConfig) -> Self {
pub fn new(config: GatewayConfig, owner_id: String) -> Self {
let auth_token = config.auth_token.clone().unwrap_or_else(|| {
use rand::RngCore;
use rand::rngs::OsRng;
@@ -83,7 +83,7 @@ impl GatewayChannel {
});
let auth = CombinedAuthState {
env_auth: MultiAuthState::single(auth_token, config.user_id.clone()),
env_auth: MultiAuthState::single(auth_token, owner_id.clone()),
db_auth: None,
};
@@ -101,8 +101,7 @@ impl GatewayChannel {
job_manager: None,
prompt_queue: None,
scheduler: None,
owner_id: config.user_id.clone(),
default_sender_id: config.user_id.clone(),
owner_id,
shutdown_tx: tokio::sync::RwLock::new(None),
ws_tracker: Some(Arc::new(ws::WsConnectionTracker::new())),
llm_provider: None,
@@ -126,18 +125,6 @@ impl GatewayChannel {
}
}
/// Rebind the single-user auth identity to the durable owner scope while
/// preserving the configured gateway sender/routing identity.
pub fn with_owner_scope(mut self, owner_id: impl Into<String>) -> Self {
let owner_id = owner_id.into();
let single_user_token = self.auth.env_auth.first_token().map(ToOwned::to_owned);
if let Some(token) = single_user_token {
self.auth.env_auth = MultiAuthState::single(token, owner_id.clone());
}
self.rebuild_state(|s| s.owner_id = owner_id);
self
}
/// Helper to rebuild state, copying existing fields and applying a mutation.
fn rebuild_state(&mut self, mutate: impl FnOnce(&mut GatewayState)) {
let mut new_state = GatewayState {
@@ -156,7 +143,6 @@ impl GatewayChannel {
prompt_queue: self.state.prompt_queue.clone(),
scheduler: self.state.scheduler.clone(),
owner_id: self.state.owner_id.clone(),
default_sender_id: self.state.default_sender_id.clone(),
shutdown_tx: tokio::sync::RwLock::new(None),
ws_tracker: self.state.ws_tracker.clone(),
llm_provider: self.state.llm_provider.clone(),
-45
View File
@@ -347,8 +347,6 @@ pub struct GatewayState {
pub prompt_queue: Option<PromptQueue>,
/// Durable owner scope for persistence and unauthenticated callback flows.
pub owner_id: String,
/// Default sender/routing identity for gateway-originated messages.
pub default_sender_id: String,
/// Shutdown signal sender.
pub shutdown_tx: tokio::sync::RwLock<Option<oneshot::Sender<()>>>,
/// WebSocket connection tracker.
@@ -407,42 +405,6 @@ pub async fn start_server(
// Public routes (no auth)
let public = Router::new()
.route("/api/health", get(health_handler))
.route("/api/debug/db-write", get({
let dbg_state = state.clone();
move || async move {
tracing::info!("debug/db-write: starting");
let store = match dbg_state.store.as_ref() {
Some(s) => s,
None => return "ERROR: store is None".to_string(),
};
tracing::info!("debug/db-write: store is Some, attempting create_user");
let id = format!("dbg-{}", uuid::Uuid::new_v4());
let now = chrono::Utc::now();
let user = crate::db::UserRecord {
id: id.clone(),
email: None,
display_name: "debug-test".to_string(),
status: "active".to_string(),
role: "member".to_string(),
created_at: now,
updated_at: now,
last_login_at: None,
created_by: None,
metadata: serde_json::json!({}),
};
match store.create_user(&user).await {
Ok(()) => {
tracing::info!("debug/db-write: create_user succeeded");
let _ = store.delete_user(&id).await;
format!("OK: created and deleted user {id}")
}
Err(e) => {
tracing::error!("debug/db-write: create_user failed: {e}");
format!("ERROR: {e}")
}
}
}
}))
.route("/oauth/callback", get(oauth_callback_handler))
.route(
"/oauth/slack/callback",
@@ -1413,9 +1375,6 @@ async fn chat_send_handler(
}
let mut msg = IncomingMessage::new("gateway", &user.user_id, &req.content);
if state.owner_id != state.default_sender_id && user.user_id == state.owner_id {
msg = msg.with_sender_id(&state.default_sender_id);
}
// Prefer timezone from JSON body, fall back to X-Timezone header
let tz = req
.timezone
@@ -1517,9 +1476,6 @@ async fn chat_approval_handler(
})?;
let mut msg = IncomingMessage::new("gateway", &user.user_id, content);
if state.owner_id != state.default_sender_id && user.user_id == state.owner_id {
msg = msg.with_sender_id(&state.default_sender_id);
}
if let Some(ref thread_id) = req.thread_id {
msg = msg.with_thread(thread_id);
@@ -3095,7 +3051,6 @@ mod tests {
job_manager: None,
prompt_queue: None,
owner_id: "test".to_string(),
default_sender_id: "test".to_string(),
shutdown_tx: tokio::sync::RwLock::new(None),
ws_tracker: None,
llm_provider: None,
-1
View File
@@ -77,7 +77,6 @@ impl TestGatewayBuilder {
job_manager: None,
prompt_queue: None,
owner_id: self.user_id.clone(),
default_sender_id: self.user_id,
shutdown_tx: tokio::sync::RwLock::new(None),
ws_tracker: Some(Arc::new(WsConnectionTracker::new())),
llm_provider: self.llm_provider,
-37
View File
@@ -16,7 +16,6 @@ use axum::routing::{delete, get, post};
use tower::ServiceExt;
use uuid::Uuid;
use crate::channels::web::GatewayChannel;
use crate::channels::web::auth::{
AuthenticatedUser, MultiAuthState, UserIdentity, auth_middleware,
};
@@ -24,7 +23,6 @@ use crate::channels::web::server::{
ActiveConfigSnapshot, GatewayState, PerUserRateLimiter, PromptQueue, RateLimiter, WorkspacePool,
};
use crate::channels::web::sse::SseManager;
use crate::config::GatewayConfig;
// ── Helpers ────────────────────────────────────────────────────────────
@@ -69,7 +67,6 @@ fn build_state(
job_manager: None,
prompt_queue,
owner_id: "test".to_string(),
default_sender_id: "test".to_string(),
shutdown_tx: tokio::sync::RwLock::new(None),
ws_tracker: None,
llm_provider: None,
@@ -88,40 +85,6 @@ fn build_state(
})
}
fn gateway_config() -> GatewayConfig {
GatewayConfig {
host: "127.0.0.1".to_string(),
port: 3000,
auth_token: Some("gateway-auth".to_string()),
user_id: "gateway-sender".to_string(),
workspace_read_scopes: Vec::new(),
memory_layers: Vec::new(),
}
}
#[test]
fn with_owner_scope_updates_gateway_owner_scope_in_multi_user_mode() {
let mut gateway = GatewayChannel::new(gateway_config());
gateway.auth = two_user_auth().into();
let gateway = gateway.with_owner_scope("owner-scope");
assert_eq!(gateway.state.owner_id, "owner-scope");
assert_eq!(gateway.state.default_sender_id, "gateway-sender");
let alice = gateway
.auth
.env_auth
.authenticate("tok-alice")
.expect("alice token should remain valid");
let bob = gateway
.auth
.env_auth
.authenticate("tok-bob")
.expect("bob token should remain valid");
assert_eq!(alice.user_id, "alice");
assert_eq!(bob.user_id, "bob");
}
/// Create a libSQL-backed test database in a temporary directory.
///
/// Returns the database and a `TempDir` guard — the database file is
-1
View File
@@ -521,7 +521,6 @@ mod tests {
prompt_queue: None,
scheduler: None,
owner_id: "test".to_string(),
default_sender_id: "test".to_string(),
shutdown_tx: tokio::sync::RwLock::new(None),
ws_tracker: Some(Arc::new(WsConnectionTracker::new())),
llm_provider: None,