Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/agent-quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 2 additions & 1 deletion docs/onboarding.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
86 changes: 64 additions & 22 deletions functions/api/[[path]].ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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) {
Expand Down
5 changes: 5 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,11 @@ function Onboarding({
<p>{agent.profile.summary || "No profile summary yet."}</p>
</div>
) : null}
{agent.status !== "approved" && agent.onboardingAuth?.status !== "verified" ? (
<p className="inline-warning">
Approval is blocked until the agent re-submits this signup handle with the operator-issued onboarding auth string.
</p>
) : null}
<footer>
<button type="button" onClick={() => onOpenProfile(agent.id)}>
Open profile
Expand Down
8 changes: 8 additions & 0 deletions src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,14 @@ meter {
color: var(--color-text-secondary);
}

.inline-warning {
margin: 0;
border-left: 4px solid var(--color-danger);
padding: 10px 12px;
color: var(--color-text);
background: color-mix(in srgb, var(--color-danger) 12%, transparent);
}

.profile-page header,
.profile-sections {
display: grid;
Expand Down
Loading