@botbuddy/cli 1.20.0 → 1.21.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbuddy/cli",
3
- "version": "1.20.0",
3
+ "version": "1.21.0",
4
4
  "description": "BotBuddy — Swarm coordination CLI for multi-agent workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,29 @@
1
+ // BOT-1582 — the per-session agent key the CLI presents to the relay for
2
+ // `botbuddy wait`/`run`/`test`/`pw`.
3
+ //
4
+ // register_agent mints this token (bound to the work-graph session it returns)
5
+ // and the relay derives agent, tenant, and the arming session from it, so a
6
+ // session that exports it needs no --profile/--session-id/--token.
7
+ //
8
+ // Prefix: `bb_agent_` + 64 lowercase hex, mirroring the server's SESSION_TOKEN_RE
9
+ // (supabase/functions/_shared/sessionToken.ts). The BOT-1572 `bb_sess_` shape is
10
+ // accepted as a legacy alias for one release (its hash is already on `sessions`).
11
+ // This is a SHAPE check only — authorization is the server's hashed lookup.
12
+ export const AGENT_KEY_RE = /^bb_(agent|sess)_[0-9a-f]{64}$/;
13
+
14
+ // BOT-1582: the env var was renamed BOTBUDDY_SESSION_TOKEN → BOTBUDDY_AGENT_KEY.
15
+ // The new name wins; the old one is honoured for one release so a session that
16
+ // still exports it keeps working. Returns null when neither is set.
17
+ //
18
+ // An EMPTY/blank value counts as unset (not as a present-but-empty token): an env
19
+ // template that declares BOTBUDDY_AGENT_KEY="" while a valid BOTBUDDY_SESSION_TOKEN
20
+ // is still exported must fall through to the legacy name, or the one-release
21
+ // compatibility guarantee breaks (Codex P2). So trim and skip blanks rather than
22
+ // `??`, which would stop at the empty string.
23
+ export function readAgentKeyEnv(env = process.env) {
24
+ const first = typeof env.BOTBUDDY_AGENT_KEY === "string" ? env.BOTBUDDY_AGENT_KEY.trim() : "";
25
+ if (first) return env.BOTBUDDY_AGENT_KEY;
26
+ const legacy = typeof env.BOTBUDDY_SESSION_TOKEN === "string" ? env.BOTBUDDY_SESSION_TOKEN.trim() : "";
27
+ if (legacy) return env.BOTBUDDY_SESSION_TOKEN;
28
+ return null;
29
+ }
@@ -17,11 +17,15 @@ export const CREDENTIAL_PREFIXES = [
17
17
  { prefix: "mcp_at_", kind: "oauth", label: "OAuth owner token" },
18
18
  { prefix: "bb_pat_", kind: "pat", label: "personal access token" },
19
19
  { prefix: "bb_cli_", kind: "cli", label: "client key" },
20
- // BOT-1573/1582 transition: TODAY every live bb_agent_ secret is an older
21
- // shared service carrier (this PR keeps them authenticating), and the actual
22
- // per-session agent token is bb_sess_ (BOT-1572). So bb_agent_ classifies as
23
- // the carrier matching the UI adapter NOT as an agent. Once BOT-1582 makes
24
- // register_agent MINT bb_agent_, this flips to the agent kind.
20
+ // BOT-1573/1582 transition: bb_agent_ is now BOTH the per-session agent token
21
+ // (BOT-1582 renamed it from bb_sess_) AND the older shared service carrier, so
22
+ // the prefix ALONE can no longer tell them apart. Classification is display
23
+ // only (never auth), and mislabelling a live carrier as an agent is worse than
24
+ // the reverse, so bb_agent_ stays classified as the carrier — matching the UI
25
+ // adapter. bb_sess_ (in-flight BOT-1572 tokens, accepted one release) is the
26
+ // one unambiguously-a-session shape, so it keeps the agent kind. A
27
+ // shape-independent way to name a live bb_agent_ session token is deferred to
28
+ // the taxonomy/status work (BOT-1573).
25
29
  { prefix: "bb_agent_", kind: "svc", label: "carrier (legacy)" },
26
30
  { prefix: "bb_sess_", kind: "agent", label: "agent (session)" },
27
31
  { prefix: "bb_ci_", kind: "ci", label: "CI key" },
@@ -15,7 +15,7 @@ function laneCoordinator(kind, call, agentId) {
15
15
  return { kind, async status({ host, slot }) { const result = await call("list_resources", { host, subtype: "playwright_lane" }); const list = Array.isArray(result) ? result : result.resources ?? []; const resource = list.find((item) => item.name === `playwright_lane:${host}:${slot}` || String(item.slot) === String(slot)); /* BOT-1490: canonical_host is a TOP-LEVEL field the server echoes even on an empty page (not-held lane); owner_is_caller is per-row. Older servers send neither → null. */ const canonicalHost = (result && !Array.isArray(result) ? result.canonical_host : null) ?? null; return resource ? { held: resource.status !== "free", heldBy: resource.owner_agent_id ?? null, heldByName: resource.agents?.name ?? null, host: resource.host ?? null, name: resource.name ?? null, slot: resource.slot ?? null, canonicalHost, ownerIsCaller: resource.owner_is_caller ?? null } : { held: false, heldBy: null, heldByName: null, host: null, name: null, slot: null, canonicalHost, ownerIsCaller: null }; }, async emit(event) { await call("record_lane_event", event); }, agentId };
16
16
  }
17
17
 
18
- // BOT-1572: authenticate lock verification with a per-session `bb_sess_` token —
18
+ // BOT-1572/1582: authenticate lock verification with a per-session `bb_agent_` token —
19
19
  // the server resolves it to the session agent (mcp-server authenticateAgent), so
20
20
  // `list_resources`/`record_lane_event` run AS the session, and holder matching
21
21
  // rides on the server's `owner_is_caller`. No machine profile is required.
package/src/pw/run.mjs CHANGED
@@ -8,6 +8,7 @@ import { resolveAgentProfile } from "../wait-profile.mjs";
8
8
  import { readProfileIdentity } from "../agent-credential-store.mjs";
9
9
  import { loadConfig, getConfig } from "../config.mjs";
10
10
  import { VERSION } from "../version.mjs";
11
+ import { readAgentKeyEnv } from "../agent-key.mjs";
11
12
  // BOT-1488: canonicalize the raw hostname the SAME way acquire_resources does
12
13
  // server-side, so the lane name bb-pw builds/matches/prints is the one the lock
13
14
  // kernel actually stored ("jonos-mbp:8", not "Jonos-MBP.localdomain:8").
@@ -23,18 +24,19 @@ function redact(value, secretValues = []) { return secretValues.reduce((text, se
23
24
  async function readRegisteredAgentId() { try { await loadConfig(); const id = getConfig()?.agent_id; return typeof id === "string" && id ? id : null; } catch { return null; } }
24
25
  async function gate({ env, host, lane, deps }) {
25
26
  if (env.BB_PW_NO_LOCK === "1") return { allowed: true };
26
- // BOT-1572: a per-session `bb_sess_` token authenticates lock verification AS
27
- // the session agent — no machine profile needed. It takes precedence over the
27
+ // BOT-1572/1582: a per-session `bb_agent_` token authenticates lock verification
28
+ // AS the session agent — no machine profile needed. It takes precedence over the
28
29
  // profile path; holder matching then rides on the server's owner_is_caller
29
30
  // (plus any local session/registered agent id). The profile path stays intact
30
- // for a session that has not adopted the token.
31
- const sessionToken = deps.sessionToken ?? env.BOTBUDDY_SESSION_TOKEN ?? null;
31
+ // for a session that has not adopted the token. $BOTBUDDY_AGENT_KEY is the norm
32
+ // ($BOTBUDDY_SESSION_TOKEN still accepted for one release).
33
+ const sessionToken = deps.sessionToken ?? readAgentKeyEnv(env);
32
34
  const sessionAgentId = deps.sessionId ?? env.BOTBUDDY_SESSION_ID ?? null;
33
35
  const registeredAgentId = await (deps.readSessionAgentId ?? readRegisteredAgentId)();
34
36
  let coordinator, selfAgentIds, callerId;
35
37
  if (sessionToken) {
36
38
  coordinator = deps.coordinator ?? createSessionTokenCoordinator({ token: sessionToken, fetchImpl: deps.fetch });
37
- if (coordinator.kind === "unverified") return { allowed: false, message: "bb-pw: $BOTBUDDY_SESSION_TOKEN is malformed. Re-register the agent, or set BB_PW_NO_LOCK=1 for local-only work." };
39
+ if (coordinator.kind === "unverified") return { allowed: false, message: "bb-pw: $BOTBUDDY_AGENT_KEY is malformed. Re-register the agent, or set BB_PW_NO_LOCK=1 for local-only work." };
38
40
  selfAgentIds = new Set([sessionAgentId, registeredAgentId, coordinator.agentId].filter(Boolean));
39
41
  callerId = coordinator.agentId ?? sessionAgentId ?? "session-token";
40
42
  } else {
@@ -99,7 +101,7 @@ export async function runPw(argv, deps = {}) {
99
101
  async function runPwInner(argv, deps = {}) {
100
102
  const env = deps.env ?? process.env, stdout = deps.stdout ?? process.stdout, stderr = deps.stderr ?? process.stderr; let args = [...argv]; if (["--help", "-h"].includes(args[0])) { help(stdout); return 0; }
101
103
  if (["--version", "-v"].includes(args[0])) { stdout.write(`${versionLine()}\n`); return 0; }
102
- // BOT-1572 (AC-8): --session-token / $BOTBUDDY_SESSION_TOKEN is accepted as a
104
+ // BOT-1572/1582 (AC-8): --session-token / $BOTBUDDY_AGENT_KEY is accepted as a
103
105
  // session identity alongside --session-id, so a token-armed session need not
104
106
  // pass an id. Holder matching still rides on the server's owner_is_caller and
105
107
  // the resolved agent ids (gate()).
package/src/run.mjs CHANGED
@@ -14,6 +14,7 @@ import { spawn } from "child_process";
14
14
  import { fileURLToPath } from "url";
15
15
  import { callToolJson } from "./api.mjs";
16
16
  import { parseLaneEvents, laneCaseCounts, flushLaneCases, buildLaneSummary, laneSummaryFilename } from "./test-lane-events.mjs";
17
+ import { AGENT_KEY_RE, readAgentKeyEnv } from "./agent-key.mjs";
17
18
 
18
19
  export const RUN_SCHEMA_VERSION = 1;
19
20
  export const EXIT = Object.freeze({ OK: 0, INVALID: 4, BACKEND: 5, INTERNAL: 7 });
@@ -24,14 +25,16 @@ const MAX_CAPTURE_BYTES = 8_192;
24
25
  // Well under AC-3's 5 s so a begin/verdict is visible within the window.
25
26
  const LANE_FLUSH_INTERVAL_MS = 2_500;
26
27
 
27
- // BOT-1572: identical to the server's session-token shape check (bb_sess_ + 64 hex).
28
- const SESSION_TOKEN_RE = /^bb_sess_[0-9a-f]{64}$/;
28
+ // BOT-1582: the server's session-token shape (bb_agent_ + 64 hex) plus the
29
+ // BOT-1572 bb_sess_ legacy alias, both accepted for one release.
30
+ const SESSION_TOKEN_RE = AGENT_KEY_RE;
29
31
 
30
32
  export function parseRunArgs(argv, env = process.env) {
31
- // BOT-1572 (AC-8): $BOTBUDDY_SESSION_TOKEN authenticates the run and carries its
32
- // session, so --session-id becomes optional (the relay derives it). $BOTBUDDY_SESSION_ID
33
+ // BOT-1572/1582 (AC-8): $BOTBUDDY_AGENT_KEY authenticates the run and carries its
34
+ // session, so --session-id becomes optional (the relay derives it). The old
35
+ // $BOTBUDDY_SESSION_TOKEN is still accepted for one release. $BOTBUDDY_SESSION_ID
33
36
  // is the default when a plain id is used (parity with `botbuddy test`).
34
- const opts = { sessionId: env.BOTBUDDY_SESSION_ID ?? null, sessionToken: env.BOTBUDDY_SESSION_TOKEN ?? null, environment: null, category: "validation", kind: "other", expectedDuration: 0, timeout: DEFAULT_TIMEOUT_SECONDS, rerunReason: null, json: false };
37
+ const opts = { sessionId: env.BOTBUDDY_SESSION_ID ?? null, sessionToken: readAgentKeyEnv(env), environment: null, category: "validation", kind: "other", expectedDuration: 0, timeout: DEFAULT_TIMEOUT_SECONDS, rerunReason: null, json: false };
35
38
  const errors = [];
36
39
  const separator = argv.indexOf("--");
37
40
  const flags = separator === -1 ? argv : argv.slice(0, separator);
@@ -66,8 +69,8 @@ export function parseRunArgs(argv, env = process.env) {
66
69
  // BOT-1572: a session token stands in for --session-id (the backend derives the
67
70
  // session from the token). A malformed token, or a plain id AND a differing
68
71
  // token, is a hard error.
69
- if (opts.sessionToken && !SESSION_TOKEN_RE.test(opts.sessionToken)) errors.push("$BOTBUDDY_SESSION_TOKEN must match bb_sess_<64 hex>");
70
- if (!opts.sessionId && !opts.sessionToken) errors.push("--session-id is required (from register_agent), or set $BOTBUDDY_SESSION_TOKEN");
72
+ if (opts.sessionToken && !SESSION_TOKEN_RE.test(opts.sessionToken)) errors.push("$BOTBUDDY_AGENT_KEY must match bb_agent_<64 hex>");
73
+ if (!opts.sessionId && !opts.sessionToken) errors.push("--session-id is required (from register_agent), or set $BOTBUDDY_AGENT_KEY");
71
74
  if (!opts.environment || !["local", "preview", "staging", "production", "none"].includes(opts.environment)) errors.push("--environment must be local, preview, staging, production, or none");
72
75
  if (!command.length) errors.push("a workload is required after --");
73
76
  if (!Number.isInteger(opts.expectedDuration) || opts.expectedDuration < 0) errors.push("--expected-duration must be a non-negative integer");
package/src/test-lane.mjs CHANGED
@@ -21,6 +21,7 @@ import { execFileSync } from "node:child_process";
21
21
 
22
22
  import { EXIT, launchRun, receiptPath, DEFAULT_TIMEOUT_SECONDS } from "./run.mjs";
23
23
  import { callToolJson } from "./api.mjs";
24
+ import { readAgentKeyEnv } from "./agent-key.mjs";
24
25
 
25
26
  // EXIT.{OK,INVALID,BACKEND,INTERNAL} plus a --wait timeout code (AC-9).
26
27
  export const EXIT_TEST = Object.freeze({ ...EXIT, TIMEOUT: 2 });
@@ -36,9 +37,10 @@ const LANE_KIND_SET = new Set(LANE_KINDS);
36
37
  export function parseTestArgs(argv, { env = process.env } = {}) {
37
38
  const opts = {
38
39
  sessionId: env.BOTBUDDY_SESSION_ID ?? null,
39
- // BOT-1572 (AC-8): a session token stands in for --session-id; it flows to the
40
- // sub-invoked `run`/`wait` which derive the session from it.
41
- sessionToken: env.BOTBUDDY_SESSION_TOKEN ?? null,
40
+ // BOT-1572/1582 (AC-8): a session token stands in for --session-id; it flows to
41
+ // the sub-invoked `run`/`wait` which derive the session from it. $BOTBUDDY_AGENT_KEY
42
+ // is the norm ($BOTBUDDY_SESSION_TOKEN still accepted for one release).
43
+ sessionToken: readAgentKeyEnv(env),
42
44
  environment: "local",
43
45
  ticket: null, pr: null, repo: null,
44
46
  laneKind: null,
@@ -136,8 +138,8 @@ export function defaultGitInfo({ cwd = process.cwd(), ticket = null, pr = null,
136
138
  // The production lane launcher: run the lane through the durable `botbuddy run`
137
139
  // worker, carrying the telemetry env into its detached child.
138
140
  async function defaultLaunchLane({ command, sessionId, sessionToken, environment, expectedDurationSeconds, childEnv, testRun, cwd, call }) {
139
- // BOT-1572: pass --session-id only when a plain id is used; a token-only lane
140
- // relies on the inherited $BOTBUDDY_SESSION_TOKEN (and an explicit --session-token
141
+ // BOT-1572/1582: pass --session-id only when a plain id is used; a token-only lane
142
+ // relies on the inherited $BOTBUDDY_AGENT_KEY (and an explicit --session-token
141
143
  // so the child never falls back to a machine credential).
142
144
  const argv = [
143
145
  ...(sessionId ? ["--session-id", sessionId] : []),
@@ -150,7 +152,7 @@ async function defaultLaunchLane({ command, sessionId, sessionToken, environment
150
152
  }
151
153
 
152
154
  function waitCommand(testRunId, sessionId, sessionToken) {
153
- // BOT-1572: a token-armed session runs the wait with only $BOTBUDDY_SESSION_TOKEN.
155
+ // BOT-1572/1582: a token-armed session runs the wait with only $BOTBUDDY_AGENT_KEY.
154
156
  const idFlag = sessionId && !sessionToken ? ` --session-id ${sessionId}` : "";
155
157
  return `botbuddy wait 'test-run:id=${testRunId}'${idFlag} --heartbeat`;
156
158
  }
@@ -169,7 +171,7 @@ export async function launchTestLane(argv, {
169
171
  return { exitCode: EXIT_TEST.INVALID, line: JSON.stringify({ outcome: "rejected", errors }) };
170
172
  }
171
173
  if (!opts.sessionId && !opts.sessionToken) {
172
- process.stderr.write("botbuddy test: --session-id is required (or set $BOTBUDDY_SESSION_ID or $BOTBUDDY_SESSION_TOKEN)\n");
174
+ process.stderr.write("botbuddy test: --session-id is required (or set $BOTBUDDY_SESSION_ID or $BOTBUDDY_AGENT_KEY)\n");
173
175
  return { exitCode: EXIT_TEST.INVALID, line: JSON.stringify({ outcome: "rejected", errors: ["--session-id is required"] }) };
174
176
  }
175
177
 
package/src/wait.mjs CHANGED
@@ -18,13 +18,14 @@ import { resolveAgentProfile, withPrincipalReceipt, PROFILE_FILE } from "./wait-
18
18
  import { VERSION } from "./version.mjs";
19
19
  import { fileURLToPath } from "node:url";
20
20
  import { latestPublicCliCommand } from "./public-invocation.mjs";
21
+ import { AGENT_KEY_RE, readAgentKeyEnv } from "./agent-key.mjs";
21
22
 
22
23
  // A protocol is deliberately distinct from package semver: compatible pinned
23
24
  // clients keep working until the server raises this minimum, while a stale
24
25
  // implementation gets a typed, safe upgrade instruction.
25
26
  // BOT-1554: protocol 2 makes --session-id mandatory for every non-timer wait (the
26
27
  // server keeps MINIMUM_WAIT_PROTOCOL=1 so pre-2 installs are not 426'd).
27
- // BOT-1572: protocol 3 — a $BOTBUDDY_SESSION_TOKEN (bb_sess_) authenticates the
28
+ // BOT-1572/1582: protocol 3 — a $BOTBUDDY_AGENT_KEY (bb_agent_) authenticates the
28
29
  // wait on its own; no profile / --session-id / --token needed (the relay derives
29
30
  // agent, tenant, and session from the token). Falls back to protocol-2 behaviour
30
31
  // when no session token is present.
@@ -33,8 +34,9 @@ const CLI_UPGRADE_COMMAND = latestPublicCliCommand("wait");
33
34
  const MIN_RECEIPT_MAX_BYTES = 512;
34
35
  // BOT-1554: identical to the server's session-id shape check.
35
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;
36
- // BOT-1572: identical to the server's session-token shape check (bb_sess_ + 64 hex).
37
- const SESSION_TOKEN_RE = /^bb_sess_[0-9a-f]{64}$/;
37
+ // BOT-1582: the server's session-token shape (bb_agent_ + 64 hex), plus the
38
+ // BOT-1572 bb_sess_ legacy alias, both accepted for one release.
39
+ const SESSION_TOKEN_RE = AGENT_KEY_RE;
38
40
 
39
41
  const HELP = `botbuddy wait — one wait command instead of a polling loop (BOT-989)
40
42
 
@@ -164,10 +166,11 @@ function parseArgv(argv) {
164
166
  // BOT-1467: attribute this wait to the arming session's agent (the id from
165
167
  // register_agent) instead of the tenant-bound profile agent. Env fallback.
166
168
  sessionId: process.env.BOTBUDDY_SESSION_ID || null,
167
- // BOT-1572: the per-session token. When present it is the ONLY credential —
168
- // profile, --session-id, and --token are unnecessary. Env is the norm; the
169
- // flag is for tests/overrides.
170
- sessionToken: process.env.BOTBUDDY_SESSION_TOKEN || null,
169
+ // BOT-1572/1582: the per-session token. When present it is the ONLY credential —
170
+ // profile, --session-id, and --token are unnecessary. $BOTBUDDY_AGENT_KEY is
171
+ // the norm ($BOTBUDDY_SESSION_TOKEN still accepted for one release); the flag
172
+ // is for tests/overrides.
173
+ sessionToken: readAgentKeyEnv(process.env),
171
174
  help: false,
172
175
  };
173
176
  for (let i = 0; i < argv.length; i++) {
@@ -659,14 +662,15 @@ function profileErrorReceipt(profile, error) {
659
662
  // renders. Keep each to one line and never embed a secret value — names, slots,
660
663
  // and env-var names only.
661
664
  const RECOVERY = Object.freeze({
662
- // $BOTBUDDY_SESSION_TOKEN (bb_sess_+64hex) is minted by register_agent; the
663
- // harness exports it at session start.
664
- sessionToken: "register_agent export BOTBUDDY_SESSION_TOKEN=<session_token> (the harness exports it at session start)",
665
+ // BOT-1582: $BOTBUDDY_AGENT_KEY (bb_agent_+64hex, renamed from
666
+ // $BOTBUDDY_SESSION_TOKEN) is minted by register_agent; the harness exports it
667
+ // at session start. The legacy var is still accepted for one release.
668
+ sessionToken: "register_agent → export BOTBUDDY_AGENT_KEY=<session_token> (the harness exports it at session start)",
665
669
  // $BOTBUDDY_SESSION_ID is the work-graph session id returned by register_agent.
666
670
  sessionId: "register_agent → export BOTBUDDY_SESSION_ID=<session_id> (the work-graph session id from register_agent)",
667
671
  // A --token / session-token contradiction: the session token is the whole
668
672
  // credential, so drop --token (never advise also setting a session id here).
669
- sessionTokenConflict: "unset --token — $BOTBUDDY_SESSION_TOKEN is the whole credential (protocol 3)",
673
+ sessionTokenConflict: "unset --token — $BOTBUDDY_AGENT_KEY is the whole credential (protocol 3)",
670
674
  // Grammar/condition errors point at the help and the canonical doc.
671
675
  conditions: "check the condition grammar: botbuddy wait --help / docs/agent-wait.md",
672
676
  });
@@ -689,7 +693,10 @@ function profileResolutionRecovery(errorCode) {
689
693
  // supported path is exporting that same profile env var directly, which
690
694
  // resolveAgentProfile reads. (The 0600 config.json store is the owner login
691
695
  // token's, not a profile credential's — a wait cannot consume it.)
692
- const HUMAN_PAT_NOTE = "human PATs (BOTBUDDY_AGENT_KEY / BOTBUDDY_AGENT_API_KEY) are not valid for waits";
696
+ // BOT-1582: $BOTBUDDY_AGENT_KEY is now the per-session token var (valid for
697
+ // waits), so it is no longer named here; BOTBUDDY_AGENT_API_KEY remains a human
698
+ // PAT that never authenticates a wait.
699
+ const HUMAN_PAT_NOTE = "human PATs (e.g. BOTBUDDY_AGENT_API_KEY) are not valid for waits";
693
700
  function profileCredentialRecovery(profile) {
694
701
  return `${profileRecovery(profile)} (stores $${profile.tokenEnv} in the macOS Keychain; on a non-Keychain host export $${profile.tokenEnv} directly); ${HUMAN_PAT_NOTE}`;
695
702
  }
@@ -737,19 +744,19 @@ export async function runWait(argv) {
737
744
  const deadlineMs = Date.now() + timeoutSec * 1000;
738
745
 
739
746
  const needsRelay = conditions.some((c) => c.type !== "timer");
740
- // BOT-1572: a $BOTBUDDY_SESSION_TOKEN authenticates the wait on its own. When
747
+ // BOT-1572/1582: a $BOTBUDDY_AGENT_KEY authenticates the wait on its own. When
741
748
  // present it supersedes the profile + --session-id path entirely: the relay
742
749
  // derives agent, tenant, and session from the token. --profile / --session-id
743
750
  // are simply ignored; a conflicting explicit --token (a DIFFERENT credential)
744
751
  // is a contradiction and is rejected.
745
752
  if (needsRelay && opts.sessionToken) {
746
753
  if (!SESSION_TOKEN_RE.test(opts.sessionToken)) {
747
- process.stderr.write(`botbuddy wait: $BOTBUDDY_SESSION_TOKEN must match bb_sess_<64 hex>; ${RECOVERY.sessionToken}\n`);
754
+ process.stderr.write(`botbuddy wait: $BOTBUDDY_AGENT_KEY must match bb_agent_<64 hex>; ${RECOVERY.sessionToken}\n`);
748
755
  emitReceipt({ schema_version: 1, outcome: "error", error: "invalid_session_token", recovery: RECOVERY.sessionToken });
749
756
  process.exit(EXIT.INVALID);
750
757
  }
751
758
  if (opts.token && opts.token !== opts.sessionToken) {
752
- process.stderr.write(`botbuddy wait: --token conflicts with $BOTBUDDY_SESSION_TOKEN; ${RECOVERY.sessionTokenConflict}\n`);
759
+ process.stderr.write(`botbuddy wait: --token conflicts with $BOTBUDDY_AGENT_KEY; ${RECOVERY.sessionTokenConflict}\n`);
753
760
  emitReceipt({ schema_version: 1, outcome: "error", error: "session_token_conflict", recovery: RECOVERY.sessionTokenConflict });
754
761
  process.exit(EXIT.INVALID);
755
762
  }
@@ -909,13 +916,13 @@ export async function runWait(argv) {
909
916
  ]);
910
917
  if (opts.useSessionToken || SESSION_TOKEN_ERRORS.has(err.errorCode)) {
911
918
  process.stderr.write(
912
- `botbuddy wait: session token rejected (${err.errorCode || "unauthorized"}) — re-register the agent and export the new $BOTBUDDY_SESSION_TOKEN\n`,
919
+ `botbuddy wait: session token rejected (${err.errorCode || "unauthorized"}) — re-register the agent and export the new $BOTBUDDY_AGENT_KEY\n`,
913
920
  );
914
921
  emitReceipt(withPrincipalReceipt({
915
922
  schema_version: 1,
916
923
  outcome: "error",
917
924
  error: err.errorCode || "unauthorized",
918
- recovery: "register_agent → export BOTBUDDY_SESSION_TOKEN=<session_token>",
925
+ recovery: "register_agent → export BOTBUDDY_AGENT_KEY=<session_token>",
919
926
  }, opts.agentProfile, { sessionTenant, agentId: registeredAgentId, sessionId: registeredSessionId }));
920
927
  process.exit(EXIT.AUTH);
921
928
  }