@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.
- package/package.json +1 -1
- package/src/wait.mjs +69 -2
package/package.json
CHANGED
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
|
-
|
|
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));
|