eleven / ocho Public

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>
Paul Campbell 9f8d15c7999c9a2ec19f6e597610030e34368d6a parent 8ea5052
2 files changed, +9 −4
  • agent/announcer.mjs +4 −2
  • agent/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 });