@botbuddy/cli 1.13.3 → 1.14.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/wait.mjs +69 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbuddy/cli",
3
- "version": "1.13.3",
3
+ "version": "1.14.0",
4
4
  "description": "BotBuddy — Swarm coordination CLI for multi-agent workflows",
5
5
  "type": "module",
6
6
  "bin": {
package/src/wait.mjs CHANGED
@@ -22,9 +22,13 @@ import { latestPublicCliCommand } from "./public-invocation.mjs";
22
22
  // A protocol is deliberately distinct from package semver: compatible pinned
23
23
  // clients keep working until the server raises this minimum, while a stale
24
24
  // implementation gets a typed, safe upgrade instruction.
25
- export const WAIT_PROTOCOL_VERSION = 1;
25
+ // BOT-1554: protocol 2 makes --session-id mandatory for every non-timer wait (the
26
+ // server keeps MINIMUM_WAIT_PROTOCOL=1 so pre-2 installs are not 426'd).
27
+ export const WAIT_PROTOCOL_VERSION = 2;
26
28
  const CLI_UPGRADE_COMMAND = latestPublicCliCommand("wait");
27
29
  const MIN_RECEIPT_MAX_BYTES = 512;
30
+ // BOT-1554: identical to the server's session-id shape check.
31
+ const SESSION_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
28
32
 
29
33
  const HELP = `botbuddy wait — one wait command instead of a polling loop (BOT-989)
30
34
 
@@ -104,7 +108,8 @@ OPTIONS
104
108
  --heartbeat keep this agent session alive while waiting (so it is not reaped)
105
109
  --url <base> relay base URL (default $BOTBUDDY_RELAY_URL or https://api.bot-buddy.ai/functions/v1)
106
110
  --profile <name> tenant-bound machine profile (normally read from .botbuddy-agent.json)
107
- --session-id <uuid> attribute this wait to the arming session (the work-graph session id from register_agent); default $BOTBUDDY_SESSION_ID
111
+ --session-id <uuid> attribute this wait to the arming session (the work-graph session id from register_agent); default $BOTBUDDY_SESSION_ID.
112
+ REQUIRED for any non-timer wait (a wait must name the work agent that armed it).
108
113
  --token <key> explicit agent key override; otherwise the profile-specific env is used
109
114
  --help show this help
110
115
 
@@ -248,6 +253,10 @@ async function registerWait(opts, conditions, deadlineIso) {
248
253
  // only ever park to timeout, so it's a hard stop, not the live-only fallback.
249
254
  "invalid_session_agent",
250
255
  "session_agent_requires_profile",
256
+ // BOT-1554: the server refuses a protocol-2 register with no session id (the
257
+ // CLI fails fast before this, but a mismatched/forced body could reach it) —
258
+ // a hard stop, never the untracked live-only fallback.
259
+ "session_id_required",
251
260
  ]);
252
261
  if (INVALID_CONDITION_CODES.has(body.error)) {
253
262
  const err = new Error(body.detail || body.error);
@@ -264,6 +273,16 @@ async function registerWait(opts, conditions, deadlineIso) {
264
273
  err.errorCode = body.error;
265
274
  throw err;
266
275
  }
276
+ // BOT-1554: the credential (or its session's agent) is a shared type='service'
277
+ // carrier — it can hold locks/MCP sessions but cannot be a wait's actor. Carry
278
+ // the carrier id so runWait can name it in the fix. Auth-shaped → exit 3.
279
+ if (body.error === "wait_actor_required") {
280
+ const err = new Error(body.detail || body.error);
281
+ err.auth = true;
282
+ err.errorCode = "wait_actor_required";
283
+ err.carrierAgentId = body.carrier_agent_id ?? null;
284
+ throw err;
285
+ }
267
286
  const err = new Error(body.detail || body.message || body.error || "forbidden");
268
287
  err.auth = true;
269
288
  err.errorCode = body.error || "forbidden";
@@ -637,6 +656,27 @@ export async function runWait(argv) {
637
656
 
638
657
  const needsRelay = conditions.some((c) => c.type !== "timer");
639
658
  if (needsRelay) {
659
+ // BOT-1554: a relay wait MUST name its arming work-graph session. Fail fast —
660
+ // before any profile resolution or network call — so a wait can never be filed
661
+ // under whatever credential the machine holds (the "/waits all Megan" bug). A
662
+ // timer-only wait needs no relay and no session id (handled by !needsRelay).
663
+ if (!opts.sessionId) {
664
+ process.stderr.write(
665
+ "botbuddy wait: --session-id is required (or set $BOTBUDDY_SESSION_ID) — the work-graph session id returned by register_agent\n",
666
+ );
667
+ emitReceipt({
668
+ schema_version: 1,
669
+ outcome: "error",
670
+ error: "session_id_required",
671
+ recovery: "register_agent → export BOTBUDDY_SESSION_ID=<session_id>",
672
+ });
673
+ process.exit(EXIT.INVALID);
674
+ }
675
+ if (!SESSION_UUID.test(opts.sessionId)) {
676
+ process.stderr.write(`botbuddy wait: --session-id must be a uuid (got '${opts.sessionId}')\n`);
677
+ emitReceipt({ schema_version: 1, outcome: "error", error: "invalid_session_agent", detail: "session_id must be a uuid" });
678
+ process.exit(EXIT.INVALID);
679
+ }
640
680
  try {
641
681
  opts.agentProfile = await resolveAgentProfile({
642
682
  explicitProfile: opts.profile,
@@ -759,6 +799,33 @@ export async function runWait(argv) {
759
799
  process.exit(EXIT.INVALID);
760
800
  }
761
801
  if (err && err.auth) {
802
+ // BOT-1554: these are all SESSION-IDENTITY failures — the supplied session id
803
+ // (or the credential behind it) is a shared service carrier, an
804
+ // ended/foreign session, or a wrong-tenant session. Re-running profile setup
805
+ // and retrying with the SAME $BOTBUDDY_SESSION_ID just repeats the 403 (that
806
+ // command can't replace the parent shell's session variable), so lead with the
807
+ // reliable fix: register a WORK agent and export its NEW session id.
808
+ const SESSION_IDENTITY_ERRORS = new Set([
809
+ "wait_actor_required", "session_agent_forbidden", "session_agent_tenant_mismatch",
810
+ ]);
811
+ if (SESSION_IDENTITY_ERRORS.has(err.errorCode)) {
812
+ const cause = err.errorCode === "wait_actor_required"
813
+ ? `the wait was armed by a shared service carrier (${err.carrierAgentId})`
814
+ : err.errorCode === "session_agent_tenant_mismatch"
815
+ ? "the session id ($BOTBUDDY_SESSION_ID) resolves to a different tenant than this wait"
816
+ : "the session id ($BOTBUDDY_SESSION_ID) is not a live agent session you own";
817
+ process.stderr.write(
818
+ `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`,
819
+ );
820
+ emitReceipt(withPrincipalReceipt({
821
+ schema_version: 1,
822
+ outcome: "error",
823
+ error: err.errorCode,
824
+ ...(err.carrierAgentId ? { carrier_agent_id: err.carrierAgentId } : {}),
825
+ recovery: "register_agent → export BOTBUDDY_SESSION_ID=<new session_id>",
826
+ }, opts.agentProfile, { sessionTenant: opts.agentProfile.tenant, agentId: null }));
827
+ process.exit(EXIT.AUTH);
828
+ }
762
829
  const error = typedProfileError(err.errorCode);
763
830
  process.stderr.write(`botbuddy wait: profile authentication failed (${error}); run '${profileRecovery(opts.agentProfile)}'\n`);
764
831
  emitReceipt(profileErrorReceipt(opts.agentProfile, error));