@botbuddy/cli 1.25.0 → 1.27.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 +2 -1
- package/src/agent-credential-store.mjs +11 -181
- package/src/api.mjs +8 -8
- package/src/auth.mjs +1 -1
- package/src/cli-credentials.mjs +11 -6
- package/src/codex-bridge.mjs +3 -3
- package/src/commands.mjs +94 -140
- package/src/config.mjs +1 -1
- package/src/mcp-key.mjs +23 -22
- package/src/pw/coordinator.mjs +0 -5
- package/src/pw/run.mjs +32 -32
- package/src/run.mjs +165 -10
- package/src/stack.mjs +3 -3
- package/src/telemetry-config.mjs +109 -0
- package/src/telemetry-delivery.mjs +40 -0
- package/src/telemetry-outbox.mjs +327 -0
- package/src/wait-profile.mjs +128 -79
- package/src/wait.mjs +88 -226
- package/src/profile-bootstrap.mjs +0 -252
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 {
|
|
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-
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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 = "
|
|
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
|
-
//
|
|
783
|
-
// whether --
|
|
784
|
-
// dual-member disambiguation
|
|
785
|
-
//
|
|
786
|
-
//
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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 &&
|
|
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
|
-
|
|
811
|
-
//
|
|
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:
|
|
835
|
-
profile:
|
|
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
|
|
914
|
-
//
|
|
915
|
-
//
|
|
916
|
-
//
|
|
917
|
-
//
|
|
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
|
-
//
|
|
935
|
-
//
|
|
936
|
-
//
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
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.
|
|
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-
|
|
1103
|
-
//
|
|
1104
|
-
//
|
|
1105
|
-
|
|
1106
|
-
|
|
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
|
|
1137
|
-
: "the session
|
|
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
|
|
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
|
|
1147
|
-
},
|
|
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
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
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 =
|
|
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
|
|
1210
|
-
// registration did not succeed, neither the server nor this client has
|
|
1211
|
-
//
|
|
1212
|
-
process.stderr.write(`bb-wait:
|
|
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
|
-
|
|
1216
|
-
{ sessionTenant:
|
|
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,
|
|
1109
|
+
withClientIdentity(withPrincipalReceipt(receipt, null, {
|
|
1248
1110
|
sessionTenant,
|
|
1249
1111
|
agentId: registeredAgentId,
|
|
1250
1112
|
sessionAgentId: registeredSessionAgentId,
|