@botbuddy/cli 1.24.0 → 1.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/wait.mjs CHANGED
@@ -14,7 +14,7 @@
14
14
  // Run botbuddy wait --help for the condition grammar.
15
15
 
16
16
  import { EXIT, parseConditions, parseSseFrames, runWaitLoop, normalizeSince, truncateReceipt, formatPrReviewSnapshotWarnings } from "./wait-core.mjs";
17
- import { resolveAgentProfile, getAgentProfile, findProfileName, findEnvProfile, withPrincipalReceipt, PROFILE_FILE } from "./wait-profile.mjs";
17
+ import { readAgentBinding, withPrincipalReceipt } from "./wait-profile.mjs";
18
18
  import { VERSION } from "./version.mjs";
19
19
  import { fileURLToPath } from "node:url";
20
20
  import { latestPublicCliCommand } from "./public-invocation.mjs";
@@ -32,8 +32,6 @@ import { AGENT_KEY_RE, readAgentKeyEnv } from "./agent-key.mjs";
32
32
  export const WAIT_PROTOCOL_VERSION = 3;
33
33
  const CLI_UPGRADE_COMMAND = latestPublicCliCommand("wait");
34
34
  const MIN_RECEIPT_MAX_BYTES = 512;
35
- // BOT-1554: identical to the server's session-id shape check.
36
- const SESSION_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
37
35
  // BOT-1582: the server's session-token shape (bb_agent_ + 64 hex), plus the
38
36
  // BOT-1572 bb_sess_ legacy alias, both accepted for one release.
39
37
  const SESSION_TOKEN_RE = AGENT_KEY_RE;
@@ -226,31 +224,18 @@ function parseArgv(argv) {
226
224
  // {waitSessionId, cursorStart} on success. Profiled waits fail closed unless the
227
225
  // relay authenticates and attests their machine principal.
228
226
  async function registerWait(opts, conditions, deadlineIso) {
229
- // BOT-1572: a session-token wait sends NO profile / expected_tenant /
230
- // session_id — the relay derives all three from the token. Everything else
231
- // (protocol-2 profile wait) is unchanged.
232
- const registerBody = opts.useSessionToken
233
- ? {
234
- action: "register",
235
- client_version: VERSION,
236
- wait_protocol_version: WAIT_PROTOCOL_VERSION,
237
- conditions,
238
- deadline: deadlineIso,
239
- mode: opts.mode,
240
- }
241
- : {
242
- action: "register",
243
- profile: opts.agentProfile.name,
244
- expected_tenant: opts.agentProfile.tenant,
245
- // BOT-1467: name the arming work-graph session so the relay resolves and attributes
246
- // the wait to its agent (validated same-owner) instead of the profile agent.
247
- ...(opts.sessionId ? { session_id: opts.sessionId } : {}),
248
- client_version: VERSION,
249
- wait_protocol_version: WAIT_PROTOCOL_VERSION,
250
- conditions,
251
- deadline: deadlineIso,
252
- mode: opts.mode,
253
- };
227
+ // BOT-1608: a relay wait authenticates from the per-session `bb_agent_` token
228
+ // ($BOTBUDDY_AGENT_KEY) — the relay derives agent, tenant, and session from it.
229
+ // The retired protocol-2 profile register (profile + expected_tenant) is gone
230
+ // with the profile bridge; runWait fails closed before here if no session token.
231
+ const registerBody = {
232
+ action: "register",
233
+ client_version: VERSION,
234
+ wait_protocol_version: WAIT_PROTOCOL_VERSION,
235
+ conditions,
236
+ deadline: deadlineIso,
237
+ mode: opts.mode,
238
+ };
254
239
  const res = await fetch(`${opts.url.replace(/\/$/, "")}/event-stream`, {
255
240
  method: "POST",
256
241
  headers: {
@@ -379,28 +364,14 @@ async function registerWait(opts, conditions, deadlineIso) {
379
364
  throw err;
380
365
  }
381
366
  const body = await res.json();
382
- if (opts.useSessionToken) {
383
- // BOT-1572: a session-token wait has no profile to attest; the relay instead
384
- // returns the derived session_id + agent_id. Their absence means the relay
385
- // did not honour the token as a session credential — fail closed.
386
- if (typeof body.agent_id !== "string" || !body.agent_id ||
387
- typeof body.session_id !== "string" || !body.session_id) {
388
- const err = new Error("relay did not attest the session token");
389
- err.auth = true;
390
- err.errorCode = "session_token_not_enforced";
391
- throw err;
392
- }
393
- } else if (body.profile !== opts.agentProfile.name || typeof body.agent_id !== "string" || !body.agent_id) {
394
- const err = new Error("relay did not attest the tenant-bound agent profile");
367
+ // BOT-1572/1608: a session-token wait has no profile to attest; the relay
368
+ // returns the derived session_id + agent_id. Their absence means the relay did
369
+ // not honour the token as a session credential fail closed.
370
+ if (typeof body.agent_id !== "string" || !body.agent_id ||
371
+ typeof body.session_id !== "string" || !body.session_id) {
372
+ const err = new Error("relay did not attest the session token");
395
373
  err.auth = true;
396
- err.errorCode = "profile_not_enforced";
397
- throw err;
398
- } else if (body.session_tenant !== opts.agentProfile.tenant) {
399
- const err = new Error(
400
- `profile expects ${opts.agentProfile.tenant} but registration resolved ${body.session_tenant ?? "no tenant"}`,
401
- );
402
- err.auth = true;
403
- err.errorCode = "profile_tenant_mismatch";
374
+ err.errorCode = "session_token_not_enforced";
404
375
  throw err;
405
376
  }
406
377
  return {
@@ -653,29 +624,6 @@ function emit(receipt, { versioned = false, maxBytes = null } = {}) {
653
624
  process.stdout.write(JSON.stringify(bounded) + "\n");
654
625
  }
655
626
 
656
- function profileRecovery(profile) {
657
- return latestPublicCliCommand(`profile setup ${profile.name}`);
658
- }
659
-
660
- function typedProfileError(errorCode) {
661
- switch (String(errorCode ?? "").toLowerCase()) {
662
- case "agent_key_required": return "profile_agent_required";
663
- case "agent_tenant_required": return "profile_credential_unbound";
664
- case "profile_tenant_mismatch": return "profile_credential_wrong_tenant";
665
- case "unauthorized": return "profile_credential_revoked";
666
- default: return errorCode;
667
- }
668
- }
669
-
670
- function profileErrorReceipt(profile, error) {
671
- return withPrincipalReceipt({
672
- schema_version: 1,
673
- outcome: "error",
674
- error,
675
- recovery: profileCredentialRecovery(profile),
676
- }, profile, { sessionTenant: profile.tenant, agentId: null });
677
- }
678
-
679
627
  // BOT-1590: canonical one-line recoveries for every setup/auth/parameter error.
680
628
  // Each names the exact command to run and the source of any missing/invalid
681
629
  // value, so an agent (or a human) can self-serve without opening the docs. The
@@ -696,32 +644,6 @@ const RECOVERY = Object.freeze({
696
644
  conditions: "check the condition grammar: botbuddy wait --help / docs/agent-wait.md",
697
645
  });
698
646
 
699
- const KNOWN_PROFILES = "botbuddy-dev, supplyguard-dev";
700
-
701
- // profile_required / unknown_profile: name the file, a minimal example, and the
702
- // known profile names so the fix is a single edit (AC-3).
703
- function profileResolutionRecovery(errorCode) {
704
- return errorCode === "unknown_profile"
705
- ? `set a known profile in ${PROFILE_FILE} or pass --profile <name> — known profiles: ${KNOWN_PROFILES}`
706
- : `add ${PROFILE_FILE} ({"schema_version":1,"profile":"botbuddy-dev"}) or pass --profile <name> — known profiles: ${KNOWN_PROFILES}`;
707
- }
708
-
709
- // A profile-credential error (missing/revoked/wrong-tenant): name the exact
710
- // setup command and the credential's real source, then rule out human PATs,
711
- // which are never valid for waits (AC-4). `botbuddy profile setup` stores the
712
- // key in the macOS Keychain under the profile env-var's name (it REQUIRES the
713
- // Keychain — ensureProfileCredentialBackend); on a non-Keychain host the
714
- // supported path is exporting that same profile env var directly, which
715
- // resolveAgentProfile reads. (The 0600 config.json store is the owner login
716
- // token's, not a profile credential's — a wait cannot consume it.)
717
- // BOT-1582: $BOTBUDDY_AGENT_KEY is now the per-session token var (valid for
718
- // waits), so it is no longer named here; BOTBUDDY_AGENT_API_KEY remains a human
719
- // PAT that never authenticates a wait.
720
- const HUMAN_PAT_NOTE = "human PATs (e.g. BOTBUDDY_AGENT_API_KEY) are not valid for waits";
721
- function profileCredentialRecovery(profile) {
722
- return `${profileRecovery(profile)} (stores $${profile.tokenEnv} in the macOS Keychain; on a non-Keychain host export $${profile.tokenEnv} directly); ${HUMAN_PAT_NOTE}`;
723
- }
724
-
725
647
  // BOT-1593: emit ONE secret-free sensor event per setup/auth/parameter failure,
726
648
  // so the maintainer can see which onboarding gap agents hit most (and whether the
727
649
  // BOT-1590 recovery copy is reducing repeats). Best-effort AND time-bounded — the
@@ -779,40 +701,26 @@ async function sendSetupErrorEvent(opts, { error, exitCode, conditions }, signal
779
701
  // P2). So only ever REUSE a token the main flow already resolved (opts.token) or
780
702
  // read the profile's env var directly — never a fresh Keychain lookup.
781
703
  //
782
- // Discover the caller's profileflag or .botbuddy-agent.json — INDEPENDENTLY of
783
- // whether --token was supplied, so profile/tenant are attached for attribution and
784
- // dual-member disambiguation even on the documented --token override (without it a
785
- // multi-tenant bearer is rejected tenant_ambiguous and the event is lost) (Codex P2).
786
- // The env-slot inference (findEnvProfile) applies ONLY when there is no explicit
787
- // token inferring a tenant for an explicit --token bearer could attach one it is
788
- // not a member of (→ tenant_forbidden drop).
789
- let profileName = opts.agentProfile?.name ?? opts.profile ?? null;
790
- if (!profileName) {
791
- try {
792
- profileName = await findProfileName(process.cwd());
793
- } catch {
794
- profileName = null; // a malformed .botbuddy-agent.json → no profile
795
- }
796
- if (!profileName && !opts.token) profileName = findEnvProfile(process.env);
704
+ // BOT-1608: discover the worktree binding — .botbuddy-agent.json — INDEPENDENTLY
705
+ // of whether an explicit --agent-key was supplied, so the tenant is attached for
706
+ // attribution and dual-member disambiguation (without it a multi-tenant bearer is
707
+ // rejected tenant_ambiguous and the event is lost). Env-only credential read: no
708
+ // Keychain lookup on this fail-fast path (it could block/prompt past the 2 s bound).
709
+ let binding = null;
710
+ try {
711
+ binding = await readAgentBinding(process.cwd());
712
+ } catch {
713
+ binding = null; // a malformed / retired-shape .botbuddy-agent.json → no binding
797
714
  }
798
- // Only a KNOWN profile contributes a name/tenant/env-var — an unknown/oversized/
799
- // sensitive raw --profile value (which the token path never validates) must not
800
- // ride the secret-free event, matching the wait path's getAgentProfile rejection
801
- // (Codex P2).
802
- const known = profileName ? getAgentProfile(profileName) : null;
803
715
  let token = opts.token;
804
- if (!token && known) token = process.env[known.tokenEnv] || null;
716
+ if (!token && binding) token = process.env[binding.mcpEnv] || null;
805
717
  // The only unusable bearer is none at all. A credential the RELAY just rejected
806
718
  // (revoked/unauthorized) will 401 at ingest too and drop silently — best-effort, by
807
719
  // design; a valid-key failure (session_id_required, wait_actor_required, wrong-tenant)
808
720
  // authenticates and records (Codex P1: this is an accepted limitation, see docs).
809
721
  if (!token) return;
810
- // "profile" auth path whenever a profile was discovered (flag/file/env slot), even
811
- // with a --token override; "explicit_token" only for a bare --token, no profile.
812
- const explicit = !!opts.token && !profileName;
813
- const tenant = opts.agentProfile?.tenant ?? known?.tenant ?? null;
814
- const safeProfile = known ? profileName : null;
815
- // Omit the explicit tenant for a wrong-tenant credential: the profile's expected
722
+ const tenant = binding?.tenant ?? null;
723
+ // Omit the explicit tenant for a wrong-tenant credential: the binding's expected
816
724
  // tenant is NOT one the bearer belongs to, and sensor-ingest's resolveWriteTenant
817
725
  // rejects an unauthorized explicit tenant (tenant_forbidden) → the event would drop.
818
726
  // Without it the server resolves the bearer's ACTUAL tenant and the row records
@@ -831,8 +739,8 @@ async function sendSetupErrorEvent(opts, { error, exitCode, conditions }, signal
831
739
  error,
832
740
  exit_code: exitCode,
833
741
  condition_types: conditionTypes,
834
- auth_path: explicit ? "explicit_token" : "profile",
835
- profile: safeProfile,
742
+ auth_path: opts.token ? "explicit_token" : "binding",
743
+ profile: null,
836
744
  cli_version: VERSION,
837
745
  },
838
746
  };
@@ -910,11 +818,11 @@ export async function runWait(argv) {
910
818
  const deadlineMs = Date.now() + timeoutSec * 1000;
911
819
 
912
820
  const needsRelay = conditions.some((c) => c.type !== "timer");
913
- // BOT-1572/1582: a $BOTBUDDY_AGENT_KEY authenticates the wait on its own. When
914
- // present it supersedes the profile + --session-id path entirely: the relay
915
- // derives agent, tenant, and session from the token. --profile / --session-id
916
- // are simply ignored; a conflicting explicit --token (a DIFFERENT credential)
917
- // is a contradiction and is rejected.
821
+ // BOT-1572/1582/1608: a relay wait authenticates from the per-session
822
+ // $BOTBUDDY_AGENT_KEY (bb_agent_) the relay derives agent, tenant, and session
823
+ // from the token. The retired protocol-2 profile register (profile + --session-id
824
+ // + a tenant-bound carrier from `profile setup`) is gone with the profile bridge,
825
+ // so a relay wait with no session token now fails closed with an actionable fix.
918
826
  if (needsRelay && opts.sessionToken) {
919
827
  if (!SESSION_TOKEN_RE.test(opts.sessionToken)) {
920
828
  process.stderr.write(`botbuddy wait: $BOTBUDDY_AGENT_KEY must match bb_agent_<64 hex>; ${RECOVERY.sessionToken}\n`);
@@ -931,54 +839,20 @@ export async function runWait(argv) {
931
839
  opts.useSessionToken = true;
932
840
  opts.token = opts.sessionToken;
933
841
  } else if (needsRelay) {
934
- // BOT-1554: a relay wait MUST name its arming work-graph session. Fail fast
935
- // before any profile resolution or network call so a wait can never be filed
936
- // under whatever credential the machine holds (the "/waits all Megan" bug). A
937
- // timer-only wait needs no relay and no session id (handled by !needsRelay).
938
- if (!opts.sessionId) {
939
- process.stderr.write(
940
- `botbuddy wait: --session-id is required (or set $BOTBUDDY_SESSION_ID) — the work-graph session id returned by register_agent; ${RECOVERY.sessionId}\n`,
941
- );
942
- emitReceipt({
943
- schema_version: 1,
944
- outcome: "error",
945
- error: "session_id_required",
946
- recovery: RECOVERY.sessionId,
947
- });
948
- await reportSetupError(opts, { error: "session_id_required", exitCode: EXIT.INVALID, conditions });
949
- process.exit(EXIT.INVALID);
950
- }
951
- if (!SESSION_UUID.test(opts.sessionId)) {
952
- process.stderr.write(`botbuddy wait: --session-id must be a uuid (got '${opts.sessionId}'); ${RECOVERY.sessionId}\n`);
953
- emitReceipt({ schema_version: 1, outcome: "error", error: "invalid_session_agent", detail: "session_id must be a uuid", recovery: RECOVERY.sessionId });
954
- await reportSetupError(opts, { error: "invalid_session_agent", exitCode: EXIT.INVALID, conditions });
955
- process.exit(EXIT.INVALID);
956
- }
957
- try {
958
- opts.agentProfile = await resolveAgentProfile({
959
- explicitProfile: opts.profile,
960
- explicitToken: opts.token,
961
- });
962
- } catch (err) {
963
- const code = err.code || "invalid_profile";
964
- const recovery = profileResolutionRecovery(code);
965
- process.stderr.write(`bb-wait: ${err.message}; ${recovery}\n`);
966
- emitReceipt({ schema_version: 1, outcome: "error", error: code, recovery });
967
- await reportSetupError(opts, { error: code, exitCode: EXIT.INVALID, conditions });
968
- process.exit(EXIT.INVALID);
969
- }
970
- opts.token = opts.agentProfile.token;
971
- if (!opts.token) {
972
- process.stderr.write(
973
- `botbuddy wait: profile '${opts.agentProfile.name}' has no tenant-bound agent credential; run ${profileCredentialRecovery(opts.agentProfile)}\n`,
974
- );
975
- emitReceipt(profileErrorReceipt(opts.agentProfile, "profile_required"));
976
- // No credential resolved (opts.token is null here), so reportSetupError has
977
- // no bearer to authenticate with and self-skips — call it for allowlist
978
- // symmetry; it is a no-op until a token exists.
979
- await reportSetupError(opts, { error: "profile_required", exitCode: EXIT.AUTH, conditions });
980
- process.exit(EXIT.AUTH);
981
- }
842
+ // No session token the wait has no credential. Fail fast (before any network
843
+ // call) so a wait is never filed under whatever machine credential is lying
844
+ // around. register_agent mints the token and the harness exports it.
845
+ process.stderr.write(
846
+ `botbuddy wait: a relay wait requires the per-session agent token — ${RECOVERY.sessionToken}, then: botbuddy wait '<condition>'\n`,
847
+ );
848
+ emitReceipt({
849
+ schema_version: 1,
850
+ outcome: "error",
851
+ error: "session_token_required",
852
+ recovery: `${RECOVERY.sessionToken}; then: botbuddy wait '<condition>'`,
853
+ });
854
+ await reportSetupError(opts, { error: "session_token_required", exitCode: EXIT.AUTH, conditions });
855
+ process.exit(EXIT.AUTH);
982
856
  }
983
857
 
984
858
  // A claim=true wait is meaningless without a tracked registration: the server
@@ -1085,8 +959,7 @@ export async function runWait(argv) {
1085
959
  if (err && err.auth) {
1086
960
  // BOT-1574 (AC4): the relay refused a wait armed by the per-machine client
1087
961
  // key (or any owner/setup credential). The fix is never profile setup —
1088
- // register a work agent and export its session token. Handle it before the
1089
- // profile-recovery branches, which would dereference an absent agentProfile.
962
+ // register a work agent and export its session token.
1090
963
  if (err.errorCode === "client_key_cannot_wait") {
1091
964
  process.stderr.write(
1092
965
  "botbuddy wait: a client key (botbuddy login) cannot arm a wait — register_agent, export BOTBUDDY_AGENT_KEY=<session_token>, then re-run\n",
@@ -1099,33 +972,11 @@ export async function runWait(argv) {
1099
972
  });
1100
973
  process.exit(EXIT.AUTH);
1101
974
  }
1102
- // BOT-1572: a session-token failure (revoked/expired/rotated, or the relay
1103
- // refusing to honour the token) is fixed by RE-REGISTERING, not by profile
1104
- // setup. Lead with that, and never touch the (absent) agentProfile.
1105
- const SESSION_TOKEN_ERRORS = new Set([
1106
- "session_token_revoked", "session_token_expired", "session_token_not_enforced",
1107
- ]);
1108
- if (opts.useSessionToken || SESSION_TOKEN_ERRORS.has(err.errorCode)) {
1109
- process.stderr.write(
1110
- `botbuddy wait: session token rejected (${err.errorCode || "unauthorized"}) — re-register the agent and export the new $BOTBUDDY_AGENT_KEY\n`,
1111
- );
1112
- emitReceipt(withPrincipalReceipt({
1113
- schema_version: 1,
1114
- outcome: "error",
1115
- error: err.errorCode || "unauthorized",
1116
- recovery: "register_agent → export BOTBUDDY_AGENT_KEY=<session_token>",
1117
- }, opts.agentProfile, { sessionTenant, agentId: registeredAgentId, sessionId: registeredSessionId }));
1118
- // Session-token path → reportSetupError self-skips (a bb_sess_ bearer
1119
- // ingest cannot authenticate); called for allowlist symmetry.
1120
- await reportSetupError(opts, { error: err.errorCode || "unauthorized", exitCode: EXIT.AUTH, conditions });
1121
- process.exit(EXIT.AUTH);
1122
- }
1123
- // BOT-1554: these are all SESSION-IDENTITY failures — the supplied session id
1124
- // (or the credential behind it) is a shared service carrier, an
1125
- // ended/foreign session, or a wrong-tenant session. Re-running profile setup
1126
- // and retrying with the SAME $BOTBUDDY_SESSION_ID just repeats the 403 (that
1127
- // command can't replace the parent shell's session variable), so lead with the
1128
- // reliable fix: register a WORK agent and export its NEW session id.
975
+ // BOT-1554: SESSION-IDENTITY failures — the session token (or the session
976
+ // id it carries) is a shared service carrier, an ended/foreign session, or
977
+ // a wrong-tenant session. Retrying with the SAME identity just repeats the
978
+ // 403, so lead with the reliable fix: register a WORK agent and export its
979
+ // NEW session token/id.
1129
980
  const SESSION_IDENTITY_ERRORS = new Set([
1130
981
  "wait_actor_required", "session_agent_forbidden", "session_agent_tenant_mismatch",
1131
982
  ]);
@@ -1133,25 +984,36 @@ export async function runWait(argv) {
1133
984
  const cause = err.errorCode === "wait_actor_required"
1134
985
  ? `the wait was armed by a shared service carrier (${err.carrierAgentId})`
1135
986
  : err.errorCode === "session_agent_tenant_mismatch"
1136
- ? "the session id ($BOTBUDDY_SESSION_ID) resolves to a different tenant than this wait"
1137
- : "the session id ($BOTBUDDY_SESSION_ID) is not a live agent session you own";
987
+ ? "the session token resolves to a different tenant than this wait"
988
+ : "the session token is not a live agent session you own";
1138
989
  process.stderr.write(
1139
- `botbuddy wait: ${cause}; register a work agent (register_agent) and export the returned id as $BOTBUDDY_SESSION_ID — re-running '${profileRecovery(opts.agentProfile)}' will not replace the shell's session variable\n`,
990
+ `botbuddy wait: ${cause}; register a work agent (register_agent) and export the returned $BOTBUDDY_AGENT_KEY (and $BOTBUDDY_SESSION_ID)\n`,
1140
991
  );
1141
992
  emitReceipt(withPrincipalReceipt({
1142
993
  schema_version: 1,
1143
994
  outcome: "error",
1144
995
  error: err.errorCode,
1145
996
  ...(err.carrierAgentId ? { carrier_agent_id: err.carrierAgentId } : {}),
1146
- recovery: "register_agent → export BOTBUDDY_SESSION_ID=<new session_id>",
1147
- }, opts.agentProfile, { sessionTenant: opts.agentProfile?.tenant ?? sessionTenant ?? null, agentId: null }));
997
+ recovery: "register_agent → export BOTBUDDY_AGENT_KEY=<session_token>",
998
+ }, null, { sessionTenant, agentId: null }));
1148
999
  await reportSetupError(opts, { error: err.errorCode, exitCode: EXIT.AUTH, conditions });
1149
1000
  process.exit(EXIT.AUTH);
1150
1001
  }
1151
- const error = typedProfileError(err.errorCode);
1152
- process.stderr.write(`botbuddy wait: profile authentication failed (${error}); run ${profileCredentialRecovery(opts.agentProfile)}\n`);
1153
- emitReceipt(profileErrorReceipt(opts.agentProfile, error));
1154
- await reportSetupError(opts, { error, exitCode: EXIT.AUTH, conditions });
1002
+ // Any other relay auth refusal (revoked/expired/rotated token, or the relay
1003
+ // refusing to honour it) is fixed by RE-REGISTERING and exporting the new
1004
+ // session token — the profile bridge and its recoveries are gone (BOT-1608).
1005
+ process.stderr.write(
1006
+ `botbuddy wait: session token rejected (${err.errorCode || "unauthorized"}) — re-register the agent and export the new $BOTBUDDY_AGENT_KEY\n`,
1007
+ );
1008
+ emitReceipt(withPrincipalReceipt({
1009
+ schema_version: 1,
1010
+ outcome: "error",
1011
+ error: err.errorCode || "unauthorized",
1012
+ recovery: "register_agent → export BOTBUDDY_AGENT_KEY=<session_token>",
1013
+ }, null, { sessionTenant, agentId: registeredAgentId, sessionId: registeredSessionId }));
1014
+ // Session-token path → reportSetupError self-skips (a bb_agent_ bearer
1015
+ // ingest cannot authenticate); called for allowlist symmetry.
1016
+ await reportSetupError(opts, { error: err.errorCode || "unauthorized", exitCode: EXIT.AUTH, conditions });
1155
1017
  process.exit(EXIT.AUTH);
1156
1018
  }
1157
1019
  if (err && err.cap) {
@@ -1169,7 +1031,7 @@ export async function runWait(argv) {
1169
1031
  // registration. Emit the typed receipt and a concrete exit: 4 (INVALID)
1170
1032
  // for a not-found key, 5 (BACKEND) for a retryable backend/config reason.
1171
1033
  const reason = err.hydration?.reason ?? (err.notFound ? "issue_not_found" : "unknown");
1172
- const tenant = opts.agentProfile?.tenant ?? "your-tenant";
1034
+ const tenant = sessionTenant ?? "your-tenant";
1173
1035
  const fix = err.notFound
1174
1036
  ? "no Linear issue with that key exists in this workspace"
1175
1037
  : reason === "no_linear_api_key"
@@ -1206,14 +1068,14 @@ export async function runWait(argv) {
1206
1068
  emitReceipt({ schema_version: 1, outcome: "error", error: "claim_registration_failed" });
1207
1069
  process.exit(EXIT.INTERNAL);
1208
1070
  }
1209
- // BOT-1338: every non-timer wait is now a tenant-bound machine profile. If
1210
- // registration did not succeed, neither the server nor this client has verified
1211
- // that profile's tenant, so there is no safe untracked fallback for ANY condition.
1212
- process.stderr.write(`bb-wait: profiled wait-session registration failed (failing closed, not arming untracked): ${err && err.message || err}\n`);
1071
+ // BOT-1338/1608: every non-timer wait is a relay-tracked, session-token wait.
1072
+ // If registration did not succeed, neither the server nor this client has
1073
+ // verified the session's tenant, so there is no safe untracked fallback.
1074
+ process.stderr.write(`bb-wait: wait-session registration failed (failing closed, not arming untracked): ${err && err.message || err}\n`);
1213
1075
  emitReceipt(withPrincipalReceipt(
1214
1076
  { schema_version: 1, outcome: "error", error: "register_failed", detail: String(err && err.message || err) },
1215
- opts.agentProfile,
1216
- { sessionTenant: opts.agentProfile?.tenant ?? sessionTenant ?? null, agentId: null },
1077
+ null,
1078
+ { sessionTenant: sessionTenant ?? null, agentId: null },
1217
1079
  ));
1218
1080
  process.exit(EXIT.INTERNAL);
1219
1081
  }
@@ -1244,7 +1106,7 @@ export async function runWait(argv) {
1244
1106
  // Emit the wake receipt FIRST — it is the terminal result the harness reads.
1245
1107
  // Finalization is best-effort telemetry and must never gate or delay it.
1246
1108
  const terminalReceipt = truncateReceipt(
1247
- withClientIdentity(withPrincipalReceipt(receipt, opts.agentProfile, {
1109
+ withClientIdentity(withPrincipalReceipt(receipt, null, {
1248
1110
  sessionTenant,
1249
1111
  agentId: registeredAgentId,
1250
1112
  sessionAgentId: registeredSessionAgentId,