agent: send claim_secret when provisioning bots (fixes bot onboarding) (#107)
The invite-claim hardening (#98) made /api/key-invite/{id}/claim require a
claim_secret — the preimage of the stored claim_hash. join.js was updated, but
the sidecar's headless join was missed: provision.mjs and announcer.mjs still
claimed with only { label }, so every NEW bot provision failed with "couldn't
claim the invite". Existing bots kept running off their persisted sessions, so
it went unnoticed until a from-scratch sidecar couldn't onboard.
Derive claimSecret from the invite secret and send it (dropping the now-ignored
label — an agent's name is set server-side when the admin mints the invite).
Mirrors join.js.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2 files changed,
+9
−4
agent/announcer.mjs+4 −2agent/provision.mjs+5 −2
diff --git a/agent/announcer.mjs b/agent/announcer.mjs| index a0f7da4..c0d2a5f 100644 |
| --- a/agent/announcer.mjs |
| +++ b/agent/announcer.mjs |
| @@ -52,8 +52,10 @@ async function loadOrProvision() { |
| headers: { "content-type": "application/json" }, |
| body: JSON.stringify(body), |
| }).then((r) => r.json()); |
| - const { id } = await deriveInvite(T); |
| - const claim = await post(`/api/key-invite/${encodeURIComponent(id)}/claim`, { label: ANNOUNCER_NAME }); |
| + const { id, claimSecret } = await deriveInvite(T); |
| + // Prove knowledge of the invite secret (claim_secret = preimage of claim_hash); |
| + // the announcer's label was set server-side when the admin minted the invite. |
| + const claim = await post(`/api/key-invite/${encodeURIComponent(id)}/claim`, { claim_secret: claimSecret }); |
| if (!claim.ok || !claim.token) throw new Error("couldn't claim the announcer invite (used or expired?)"); |
| const kp = await generateUserKeypair(); |
| await post("/api/pubkey", { token: claim.token, public_key: kp.publicKey }); |
diff --git a/agent/provision.mjs b/agent/provision.mjs| index 6649e84..094a752 100644 |
| --- a/agent/provision.mjs |
| +++ b/agent/provision.mjs |
| @@ -23,13 +23,16 @@ export async function provision(T, { name = AGENT_NAME, path = SESSION_PATH } = |
| body: JSON.stringify(body), |
| }).then((r) => r.json()); |
| - const { id } = await deriveInvite(T); |
| + const { id, claimSecret } = await deriveInvite(T); |
| const info = await (await fetch(`${BASE}/api/key-invite/${encodeURIComponent(id)}`)).json(); |
| if (!info.ok) throw new Error("invite is used or invalid — mint a fresh one"); |
| if (info.wrapped) throw new Error("that's an add-device link; use a fresh-person invite"); |
| const kp = await generateUserKeypair(); |
| - const claim = await post(`/api/key-invite/${encodeURIComponent(id)}/claim`, { label: name }); |
| + // The claim must prove knowledge of the invite secret (claim_secret is the |
| + // preimage of the stored claim_hash). The screen name is never sent — an agent's |
| + // label was set on the server when the admin minted the invite. (Mirrors join.js.) |
| + const claim = await post(`/api/key-invite/${encodeURIComponent(id)}/claim`, { claim_secret: claimSecret }); |
| if (!claim.ok || !claim.token) throw new Error("couldn't claim the invite (it may have just been used)"); |
| await post("/api/pubkey", { token: claim.token, public_key: kp.publicKey }); |