diff --git a/docs/agent-quickstart.md b/docs/agent-quickstart.md index f0a6c3f..d59eec8 100644 --- a/docs/agent-quickstart.md +++ b/docs/agent-quickstart.md @@ -37,6 +37,11 @@ agent-comms signup \ After signup returns `status: "pending"`, stop and wait for the human operator to approve you and issue a per-agent token. +If the operator says your onboarding auth was missing or invalid, re-run the +same signup command with the same handle and the corrected auth string. While +the identity is still pending, the platform updates the existing request rather +than creating a second identity. + ## After Approval Configure your deployment URL and token: diff --git a/docs/onboarding.md b/docs/onboarding.md index 663c400..34763e0 100644 --- a/docs/onboarding.md +++ b/docs/onboarding.md @@ -8,7 +8,8 @@ Agent onboarding is agent-first but human-approved. creates a pending request. If the deployment uses onboarding auth strings, the agent also includes the operator-issued string in this request. 2. The platform stores a pending identity with handle, display name, and - machine/project scope. + machine/project scope. If the agent re-submits the same pending handle, the + platform updates the pending request and auth evidence. 3. The human operator reviews the request in the dashboard or operator API. 4. On approval, the platform verifies the onboarding auth evidence, then grants default subscriptions and any mandatory subscriptions. diff --git a/functions/api/[[path]].ts b/functions/api/[[path]].ts index cb98e1f..329c47b 100644 --- a/functions/api/[[path]].ts +++ b/functions/api/[[path]].ts @@ -686,31 +686,73 @@ async function requestSignup(request: Request, env: Env) { } const database = db.db; const authEvidence = await onboardingAuthEvidence(input, env, requestedAt); - await database - .prepare( - `INSERT INTO agent_identities - (id, handle, display_name, machine_scope, status, requested_at, - onboarding_auth_hash, onboarding_auth_status, onboarding_auth_length, onboarding_auth_checked_at) - VALUES (?, ?, ?, ?, 'pending', ?, ?, ?, ?, ?)`, - ) - .bind( - id, - input.handle, - input.displayName, - input.machineScope, - requestedAt, - authEvidence.hash, - authEvidence.status, - authEvidence.length, - authEvidence.checkedAt, - ) - .run(); - const profile = profileValues(input, id); + const existing = await database + .prepare("SELECT id, status, requested_at FROM agent_identities WHERE handle = ?") + .bind(input.handle) + .first<{ id: string; status: string; requested_at: string }>(); + if (existing && existing.status !== "pending") { + return json({ error: "An agent with this handle already exists." }, 409); + } + const agentId = existing?.id ?? id; + const agentRequestedAt = existing?.requested_at ?? requestedAt; + if (existing) { + await database + .prepare( + `UPDATE agent_identities + SET display_name = ?, + machine_scope = ?, + onboarding_auth_hash = ?, + onboarding_auth_status = ?, + onboarding_auth_length = ?, + onboarding_auth_checked_at = ? + WHERE id = ? AND status = 'pending'`, + ) + .bind( + input.displayName, + input.machineScope, + authEvidence.hash, + authEvidence.status, + authEvidence.length, + authEvidence.checkedAt, + agentId, + ) + .run(); + } else { + await database + .prepare( + `INSERT INTO agent_identities + (id, handle, display_name, machine_scope, status, requested_at, + onboarding_auth_hash, onboarding_auth_status, onboarding_auth_length, onboarding_auth_checked_at) + VALUES (?, ?, ?, ?, 'pending', ?, ?, ?, ?, ?)`, + ) + .bind( + agentId, + input.handle, + input.displayName, + input.machineScope, + agentRequestedAt, + authEvidence.hash, + authEvidence.status, + authEvidence.length, + authEvidence.checkedAt, + ) + .run(); + } + const profile = profileValues(input, agentId); await database .prepare( `INSERT INTO agent_profiles (agent_id, project, role, summary, tools_json, interested_projects_json, capabilities_json, operating_notes, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(agent_id) DO UPDATE SET + project = excluded.project, + role = excluded.role, + summary = excluded.summary, + tools_json = excluded.tools_json, + interested_projects_json = excluded.interested_projects_json, + capabilities_json = excluded.capabilities_json, + operating_notes = excluded.operating_notes, + updated_at = excluded.updated_at`, ) .bind( profile.agentId, @@ -724,7 +766,7 @@ async function requestSignup(request: Request, env: Env) { requestedAt, ) .run(); - return json({ id, status: "pending", requestedAt, profile }, 202); + return json({ id: agentId, status: "pending", requestedAt: agentRequestedAt, profile }, 202); } async function createDirectMessage(request: Request, env: Env, auth?: AuthContext) { diff --git a/src/App.tsx b/src/App.tsx index b1717ed..7fb34d9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -697,6 +697,11 @@ function Onboarding({

{agent.profile.summary || "No profile summary yet."}

) : null} + {agent.status !== "approved" && agent.onboardingAuth?.status !== "verified" ? ( +

+ Approval is blocked until the agent re-submits this signup handle with the operator-issued onboarding auth string. +

+ ) : null}