@botbuddy/cli 1.21.0 → 1.22.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/stack.mjs +19 -0
- package/src/wait-profile.mjs +13 -0
- package/src/wait.mjs +143 -1
package/package.json
CHANGED
package/src/stack.mjs
CHANGED
|
@@ -31,6 +31,7 @@ import { callToolJson } from "./api.mjs";
|
|
|
31
31
|
import { SERVER_URL, getConfig } from "./config.mjs";
|
|
32
32
|
import { resolveOwnerToken, resolveAgentKey } from "./cli-credentials.mjs";
|
|
33
33
|
import { runDockerCommand, runDockerWorkflow } from "./docker-hygiene.mjs";
|
|
34
|
+
import { machineUuid } from "./machine-id.mjs";
|
|
34
35
|
import { bold, dim, yellow } from "./utils.mjs";
|
|
35
36
|
|
|
36
37
|
export const STACK_SCHEMA_VERSION = 1;
|
|
@@ -667,6 +668,7 @@ export async function cmdUp(opts, {
|
|
|
667
668
|
localProvisionFn = localProvision,
|
|
668
669
|
waitFn = waitForLease,
|
|
669
670
|
emitResult = emit,
|
|
671
|
+
machineUuidFn = machineUuid,
|
|
670
672
|
} = {}) {
|
|
671
673
|
let slot;
|
|
672
674
|
try { slot = deriveSlot(opts); } catch (e) {
|
|
@@ -696,6 +698,13 @@ export async function cmdUp(opts, {
|
|
|
696
698
|
const auth = await authProvider();
|
|
697
699
|
if (!auth) return emitResult(buildReceipt({ command: "up", outcome: "error", error: "not authenticated — run `botbuddy login`" }), opts, EXIT.AUTH);
|
|
698
700
|
|
|
701
|
+
// BOT-1585: co-location dispatches the lease to the Helper enrolled for THIS
|
|
702
|
+
// physical machine (hardware id), since a hostname is not machine-unique. The
|
|
703
|
+
// server requires it; fail fast with an actionable message if it can't be read.
|
|
704
|
+
const hardwareUuid = machineUuidFn();
|
|
705
|
+
if (!hardwareUuid) {
|
|
706
|
+
return emitResult(buildReceipt({ command: "up", outcome: "error", code: "MACHINE_UUID_REQUIRED", error: "could not determine this machine's hardware id (needed to dispatch the stack lease to the right machine)" }), opts, EXIT.BACKEND);
|
|
707
|
+
}
|
|
699
708
|
const req = await callTool("request_stack_lease", {
|
|
700
709
|
slot, host_key: opts.host || undefined, repo: opts.repo || undefined,
|
|
701
710
|
ticket_id: opts.ticket || undefined, ticket_url: opts.ticketUrl || undefined,
|
|
@@ -703,6 +712,7 @@ export async function cmdUp(opts, {
|
|
|
703
712
|
purpose: opts.purpose || undefined, idle_ttl_secs: opts.idleTtl ?? undefined,
|
|
704
713
|
stack_path: execution.stackPath,
|
|
705
714
|
worktree_root: execution.worktreeRoot,
|
|
715
|
+
machine_uuid: hardwareUuid,
|
|
706
716
|
});
|
|
707
717
|
if (!req.ok) {
|
|
708
718
|
return req.auth
|
|
@@ -1046,6 +1056,7 @@ export async function runStackLifecycle(opts, childArgv, adapters = {}) {
|
|
|
1046
1056
|
release: (leaseId, signal) => callToolJson("release_stack_lease", { lease_id: leaseId, disposition: "destroy" }, { signal }),
|
|
1047
1057
|
};
|
|
1048
1058
|
const auth = adapters.auth ?? await stackAuthHeader();
|
|
1059
|
+
const machineUuidFn = adapters.machineUuidFn ?? machineUuid;
|
|
1049
1060
|
const wait = adapters.wait ?? ((leaseId, done, failed, options) => waitForLease(leaseId, done, failed, options));
|
|
1050
1061
|
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process -- validated executable + argv only; shell is never used.
|
|
1051
1062
|
const startChild = adapters.startChild ?? ((argv, env, cwd) => spawn(argv[0], argv.slice(1), { cwd, env, stdio: "inherit", detached: true }));
|
|
@@ -1122,6 +1133,13 @@ export async function runStackLifecycle(opts, childArgv, adapters = {}) {
|
|
|
1122
1133
|
|
|
1123
1134
|
try {
|
|
1124
1135
|
if (!auth) return { exitCode: EXIT.AUTH, outcome: "error", error: "not authenticated — run botbuddy profile setup botbuddy-dev" };
|
|
1136
|
+
// BOT-1585: request_stack_lease requires this machine's hardware id so the lease
|
|
1137
|
+
// dispatches to the machine the worktree was registered on. `stack run` builds its
|
|
1138
|
+
// own payload (separate from `cmdUp`), so it must send it too.
|
|
1139
|
+
const hardwareUuid = machineUuidFn();
|
|
1140
|
+
if (!hardwareUuid) {
|
|
1141
|
+
return { exitCode: EXIT.BACKEND, outcome: "error", error: "could not determine this machine's hardware id (needed to dispatch the stack lease to the right machine)" };
|
|
1142
|
+
}
|
|
1125
1143
|
const slot = deriveSlot(opts);
|
|
1126
1144
|
const execution = resolveStackPath(process.cwd(), opts.stackPath);
|
|
1127
1145
|
const request = await api.request({
|
|
@@ -1129,6 +1147,7 @@ export async function runStackLifecycle(opts, childArgv, adapters = {}) {
|
|
|
1129
1147
|
ticket_url: opts.ticketUrl || undefined, pr_id: opts.prId || undefined, pr_url: opts.prUrl || undefined,
|
|
1130
1148
|
purpose: opts.purpose || "stack run", idle_ttl_secs: opts.idleTtl ?? undefined,
|
|
1131
1149
|
stack_path: execution.stackPath, worktree_root: execution.worktreeRoot,
|
|
1150
|
+
machine_uuid: hardwareUuid,
|
|
1132
1151
|
});
|
|
1133
1152
|
if (!request?.ok) return { exitCode: request?.auth ? EXIT.AUTH : EXIT.BACKEND, outcome: "error", error: request?.error || "lease request failed" };
|
|
1134
1153
|
if (!request.data?.success) return { exitCode: EXIT.BACKEND, outcome: "error", error: request.data?.message || request.data?.code || "lease request refused" };
|
package/src/wait-profile.mjs
CHANGED
|
@@ -20,6 +20,19 @@ export function getAgentProfile(name) {
|
|
|
20
20
|
return PROFILES[name] ?? null;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
// BOT-1593: when no profile is named (no --profile, no .botbuddy-agent.json) but
|
|
24
|
+
// EXACTLY ONE supported profile's credential env var is populated, that slot is the
|
|
25
|
+
// unambiguous profile. Used only by best-effort setup-error telemetry to attribute a
|
|
26
|
+
// `profile_required` failure that still has a usable env bearer — never for the wait
|
|
27
|
+
// itself. Returns the profile name, or null when zero or more than one slot is set
|
|
28
|
+
// (ambiguous → no attribution). Reads env only; never touches the Keychain.
|
|
29
|
+
export function findEnvProfile(env = process.env) {
|
|
30
|
+
const populated = Object.entries(PROFILES).filter(
|
|
31
|
+
([, { tokenEnv }]) => typeof env[tokenEnv] === "string" && env[tokenEnv].length > 0,
|
|
32
|
+
);
|
|
33
|
+
return populated.length === 1 ? populated[0][0] : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
23
36
|
export async function findProfileName(cwd) {
|
|
24
37
|
let dir = cwd;
|
|
25
38
|
const root = parse(dir).root;
|
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, withPrincipalReceipt, PROFILE_FILE } from "./wait-profile.mjs";
|
|
17
|
+
import { resolveAgentProfile, getAgentProfile, findProfileName, findEnvProfile, withPrincipalReceipt, PROFILE_FILE } 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";
|
|
@@ -701,6 +701,131 @@ function profileCredentialRecovery(profile) {
|
|
|
701
701
|
return `${profileRecovery(profile)} (stores $${profile.tokenEnv} in the macOS Keychain; on a non-Keychain host export $${profile.tokenEnv} directly); ${HUMAN_PAT_NOTE}`;
|
|
702
702
|
}
|
|
703
703
|
|
|
704
|
+
// BOT-1593: emit ONE secret-free sensor event per setup/auth/parameter failure,
|
|
705
|
+
// so the maintainer can see which onboarding gap agents hit most (and whether the
|
|
706
|
+
// BOT-1590 recovery copy is reducing repeats). Best-effort AND time-bounded — the
|
|
707
|
+
// same stance as docker-hygiene's reportReliability: the receipt has already been
|
|
708
|
+
// written to stdout by the time this runs, so a slow/failing POST must never delay
|
|
709
|
+
// the agent's wake or change the exit path. A 2 s AbortController caps the fetch;
|
|
710
|
+
// timeout / network error / non-2xx are all a silent drop (we never read the body).
|
|
711
|
+
//
|
|
712
|
+
// Never posts without a usable bearer, and never on the session-token path: the
|
|
713
|
+
// ingest endpoint authenticates only allowed_users PATs or agent keys
|
|
714
|
+
// (edgeAuth.ts) — a bb_sess_ session token 401s, so we don't spend the round-trip.
|
|
715
|
+
// The event carries NO secret by construction: no token (only the bearer header,
|
|
716
|
+
// never the body), no session id, no condition params/values, no hostnames/paths —
|
|
717
|
+
// only error code, exit code, condition TYPES, auth path, profile name, cli version.
|
|
718
|
+
const SENSOR_TELEMETRY_TIMEOUT_MS = 2000;
|
|
719
|
+
|
|
720
|
+
async function reportSetupError(opts, { error, exitCode, conditions }) {
|
|
721
|
+
// Never on the session-token path. A bb_agent_/bb_sess_ SESSION token cannot
|
|
722
|
+
// authenticate at sensor-ingest (edgeAuth resolves only PATs / agent keys), so it
|
|
723
|
+
// would only 401. Discriminate by the credential's SOURCE, not its shape: a
|
|
724
|
+
// session token arrives only via $BOTBUDDY_AGENT_KEY / --session-token
|
|
725
|
+
// (opts.sessionToken), whereas a resolved PROFILE credential may legitimately be a
|
|
726
|
+
// legacy bb_agent_ CARRIER key that ingest CAN authenticate — the two share a shape
|
|
727
|
+
// (BOT-1582), so a shape check would wrongly drop the carrier case (Codex P1).
|
|
728
|
+
if (opts.sessionToken) return;
|
|
729
|
+
|
|
730
|
+
// Time-box the WHOLE operation — the implicit-profile file lookup AND the fetch —
|
|
731
|
+
// against one 2 s abort, the reportReliability idiom. findProfileName walks up the
|
|
732
|
+
// directory tree with async readFile; on a slow/unresponsive network or FUSE mount
|
|
733
|
+
// that read can hang, and it runs on fail-fast paths right before process.exit, so
|
|
734
|
+
// starting the timer FIRST and racing the whole worker keeps a nominally fail-fast
|
|
735
|
+
// error inside the advertised bound (Codex P2). On expiry the race resolves and we
|
|
736
|
+
// return; the orphaned lookup is reaped by the imminent process.exit.
|
|
737
|
+
const ac = new AbortController();
|
|
738
|
+
let timer;
|
|
739
|
+
const TIMED_OUT = Symbol("sensor-telemetry-timeout");
|
|
740
|
+
const timeout = new Promise((resolve) => {
|
|
741
|
+
timer = setTimeout(() => { ac.abort(); resolve(TIMED_OUT); }, SENSOR_TELEMETRY_TIMEOUT_MS);
|
|
742
|
+
});
|
|
743
|
+
try {
|
|
744
|
+
await Promise.race([sendSetupErrorEvent(opts, { error, exitCode, conditions }, ac.signal), timeout]);
|
|
745
|
+
} catch {
|
|
746
|
+
// Best-effort: an AbortError (2 s timeout) or any transport/lookup error drops.
|
|
747
|
+
} finally {
|
|
748
|
+
clearTimeout(timer);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
async function sendSetupErrorEvent(opts, { error, exitCode, conditions }, signal) {
|
|
753
|
+
// Resolve a bearer WITHOUT ever touching the Keychain. This telemetry is
|
|
754
|
+
// best-effort and runs on fail-fast paths right before process.exit; a Keychain
|
|
755
|
+
// read (readProfileCredential → `security find-generic-password`) can block/prompt
|
|
756
|
+
// on a locked or non--A item AND takes no abort signal, so it would leave an
|
|
757
|
+
// orphaned `security` child or an auth dialog behind after the wait exits (Codex
|
|
758
|
+
// P2). So only ever REUSE a token the main flow already resolved (opts.token) or
|
|
759
|
+
// read the profile's env var directly — never a fresh Keychain lookup.
|
|
760
|
+
//
|
|
761
|
+
// Discover the caller's profile — flag or .botbuddy-agent.json — INDEPENDENTLY of
|
|
762
|
+
// whether --token was supplied, so profile/tenant are attached for attribution and
|
|
763
|
+
// dual-member disambiguation even on the documented --token override (without it a
|
|
764
|
+
// multi-tenant bearer is rejected tenant_ambiguous and the event is lost) (Codex P2).
|
|
765
|
+
// The env-slot inference (findEnvProfile) applies ONLY when there is no explicit
|
|
766
|
+
// token — inferring a tenant for an explicit --token bearer could attach one it is
|
|
767
|
+
// not a member of (→ tenant_forbidden drop).
|
|
768
|
+
let profileName = opts.agentProfile?.name ?? opts.profile ?? null;
|
|
769
|
+
if (!profileName) {
|
|
770
|
+
try {
|
|
771
|
+
profileName = await findProfileName(process.cwd());
|
|
772
|
+
} catch {
|
|
773
|
+
profileName = null; // a malformed .botbuddy-agent.json → no profile
|
|
774
|
+
}
|
|
775
|
+
if (!profileName && !opts.token) profileName = findEnvProfile(process.env);
|
|
776
|
+
}
|
|
777
|
+
// Only a KNOWN profile contributes a name/tenant/env-var — an unknown/oversized/
|
|
778
|
+
// sensitive raw --profile value (which the token path never validates) must not
|
|
779
|
+
// ride the secret-free event, matching the wait path's getAgentProfile rejection
|
|
780
|
+
// (Codex P2).
|
|
781
|
+
const known = profileName ? getAgentProfile(profileName) : null;
|
|
782
|
+
let token = opts.token;
|
|
783
|
+
if (!token && known) token = process.env[known.tokenEnv] || null;
|
|
784
|
+
// The only unusable bearer is none at all. A credential the RELAY just rejected
|
|
785
|
+
// (revoked/unauthorized) will 401 at ingest too and drop silently — best-effort, by
|
|
786
|
+
// design; a valid-key failure (session_id_required, wait_actor_required, wrong-tenant)
|
|
787
|
+
// authenticates and records (Codex P1: this is an accepted limitation, see docs).
|
|
788
|
+
if (!token) return;
|
|
789
|
+
// "profile" auth path whenever a profile was discovered (flag/file/env slot), even
|
|
790
|
+
// with a --token override; "explicit_token" only for a bare --token, no profile.
|
|
791
|
+
const explicit = !!opts.token && !profileName;
|
|
792
|
+
const tenant = opts.agentProfile?.tenant ?? known?.tenant ?? null;
|
|
793
|
+
const safeProfile = known ? profileName : null;
|
|
794
|
+
// Omit the explicit tenant for a wrong-tenant credential: the profile's expected
|
|
795
|
+
// tenant is NOT one the bearer belongs to, and sensor-ingest's resolveWriteTenant
|
|
796
|
+
// rejects an unauthorized explicit tenant (tenant_forbidden) → the event would drop.
|
|
797
|
+
// Without it the server resolves the bearer's ACTUAL tenant and the row records
|
|
798
|
+
// there instead (Codex P1). Every other error's tenant equals the credential's own.
|
|
799
|
+
const includeTenant = tenant && error !== "profile_credential_wrong_tenant";
|
|
800
|
+
const conditionTypes = Array.isArray(conditions)
|
|
801
|
+
? [...new Set(conditions.map((c) => c.type))].sort()
|
|
802
|
+
: [];
|
|
803
|
+
const event = {
|
|
804
|
+
...(includeTenant ? { tenant } : {}),
|
|
805
|
+
source: "bb-wait",
|
|
806
|
+
kind: "wait_setup_error",
|
|
807
|
+
dedupe_key: `bb-wait:${error}:${crypto.randomUUID()}`,
|
|
808
|
+
occurred_at: new Date().toISOString(),
|
|
809
|
+
payload: {
|
|
810
|
+
error,
|
|
811
|
+
exit_code: exitCode,
|
|
812
|
+
condition_types: conditionTypes,
|
|
813
|
+
auth_path: explicit ? "explicit_token" : "profile",
|
|
814
|
+
profile: safeProfile,
|
|
815
|
+
cli_version: VERSION,
|
|
816
|
+
},
|
|
817
|
+
};
|
|
818
|
+
await fetch(`${opts.url.replace(/\/$/, "")}/sensor-ingest`, {
|
|
819
|
+
method: "POST",
|
|
820
|
+
headers: {
|
|
821
|
+
Authorization: `Bearer ${token}`,
|
|
822
|
+
"Content-Type": "application/json",
|
|
823
|
+
},
|
|
824
|
+
body: JSON.stringify(event),
|
|
825
|
+
signal,
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
|
|
704
829
|
export async function runWait(argv) {
|
|
705
830
|
const opts = parseArgv(argv);
|
|
706
831
|
const emitReceipt = (receipt, options = {}) => emit(receipt, {
|
|
@@ -727,6 +852,9 @@ export async function runWait(argv) {
|
|
|
727
852
|
for (const e of errors) process.stderr.write(`bb-wait: invalid condition '${e.spec}': ${e.message}\n`);
|
|
728
853
|
process.stderr.write(`bb-wait: ${RECOVERY.conditions}\n`);
|
|
729
854
|
emitReceipt({ schema_version: 1, outcome: "error", error: "invalid_conditions", errors, recovery: RECOVERY.conditions });
|
|
855
|
+
// BOT-1593: condition_types is [] on a grammar failure (the parsed set is
|
|
856
|
+
// partial/empty and is caller data regardless).
|
|
857
|
+
await reportSetupError(opts, { error: "invalid_conditions", exitCode: EXIT.INVALID, conditions: [] });
|
|
730
858
|
process.exit(EXIT.INVALID);
|
|
731
859
|
}
|
|
732
860
|
|
|
@@ -753,11 +881,13 @@ export async function runWait(argv) {
|
|
|
753
881
|
if (!SESSION_TOKEN_RE.test(opts.sessionToken)) {
|
|
754
882
|
process.stderr.write(`botbuddy wait: $BOTBUDDY_AGENT_KEY must match bb_agent_<64 hex>; ${RECOVERY.sessionToken}\n`);
|
|
755
883
|
emitReceipt({ schema_version: 1, outcome: "error", error: "invalid_session_token", recovery: RECOVERY.sessionToken });
|
|
884
|
+
await reportSetupError(opts, { error: "invalid_session_token", exitCode: EXIT.INVALID, conditions });
|
|
756
885
|
process.exit(EXIT.INVALID);
|
|
757
886
|
}
|
|
758
887
|
if (opts.token && opts.token !== opts.sessionToken) {
|
|
759
888
|
process.stderr.write(`botbuddy wait: --token conflicts with $BOTBUDDY_AGENT_KEY; ${RECOVERY.sessionTokenConflict}\n`);
|
|
760
889
|
emitReceipt({ schema_version: 1, outcome: "error", error: "session_token_conflict", recovery: RECOVERY.sessionTokenConflict });
|
|
890
|
+
await reportSetupError(opts, { error: "session_token_conflict", exitCode: EXIT.INVALID, conditions });
|
|
761
891
|
process.exit(EXIT.INVALID);
|
|
762
892
|
}
|
|
763
893
|
opts.useSessionToken = true;
|
|
@@ -777,11 +907,13 @@ export async function runWait(argv) {
|
|
|
777
907
|
error: "session_id_required",
|
|
778
908
|
recovery: RECOVERY.sessionId,
|
|
779
909
|
});
|
|
910
|
+
await reportSetupError(opts, { error: "session_id_required", exitCode: EXIT.INVALID, conditions });
|
|
780
911
|
process.exit(EXIT.INVALID);
|
|
781
912
|
}
|
|
782
913
|
if (!SESSION_UUID.test(opts.sessionId)) {
|
|
783
914
|
process.stderr.write(`botbuddy wait: --session-id must be a uuid (got '${opts.sessionId}'); ${RECOVERY.sessionId}\n`);
|
|
784
915
|
emitReceipt({ schema_version: 1, outcome: "error", error: "invalid_session_agent", detail: "session_id must be a uuid", recovery: RECOVERY.sessionId });
|
|
916
|
+
await reportSetupError(opts, { error: "invalid_session_agent", exitCode: EXIT.INVALID, conditions });
|
|
785
917
|
process.exit(EXIT.INVALID);
|
|
786
918
|
}
|
|
787
919
|
try {
|
|
@@ -794,6 +926,7 @@ export async function runWait(argv) {
|
|
|
794
926
|
const recovery = profileResolutionRecovery(code);
|
|
795
927
|
process.stderr.write(`bb-wait: ${err.message}; ${recovery}\n`);
|
|
796
928
|
emitReceipt({ schema_version: 1, outcome: "error", error: code, recovery });
|
|
929
|
+
await reportSetupError(opts, { error: code, exitCode: EXIT.INVALID, conditions });
|
|
797
930
|
process.exit(EXIT.INVALID);
|
|
798
931
|
}
|
|
799
932
|
opts.token = opts.agentProfile.token;
|
|
@@ -802,6 +935,10 @@ export async function runWait(argv) {
|
|
|
802
935
|
`botbuddy wait: profile '${opts.agentProfile.name}' has no tenant-bound agent credential; run ${profileCredentialRecovery(opts.agentProfile)}\n`,
|
|
803
936
|
);
|
|
804
937
|
emitReceipt(profileErrorReceipt(opts.agentProfile, "profile_required"));
|
|
938
|
+
// No credential resolved (opts.token is null here), so reportSetupError has
|
|
939
|
+
// no bearer to authenticate with and self-skips — call it for allowlist
|
|
940
|
+
// symmetry; it is a no-op until a token exists.
|
|
941
|
+
await reportSetupError(opts, { error: "profile_required", exitCode: EXIT.AUTH, conditions });
|
|
805
942
|
process.exit(EXIT.AUTH);
|
|
806
943
|
}
|
|
807
944
|
}
|
|
@@ -924,6 +1061,9 @@ export async function runWait(argv) {
|
|
|
924
1061
|
error: err.errorCode || "unauthorized",
|
|
925
1062
|
recovery: "register_agent → export BOTBUDDY_AGENT_KEY=<session_token>",
|
|
926
1063
|
}, opts.agentProfile, { sessionTenant, agentId: registeredAgentId, sessionId: registeredSessionId }));
|
|
1064
|
+
// Session-token path → reportSetupError self-skips (a bb_sess_ bearer
|
|
1065
|
+
// ingest cannot authenticate); called for allowlist symmetry.
|
|
1066
|
+
await reportSetupError(opts, { error: err.errorCode || "unauthorized", exitCode: EXIT.AUTH, conditions });
|
|
927
1067
|
process.exit(EXIT.AUTH);
|
|
928
1068
|
}
|
|
929
1069
|
// BOT-1554: these are all SESSION-IDENTITY failures — the supplied session id
|
|
@@ -951,11 +1091,13 @@ export async function runWait(argv) {
|
|
|
951
1091
|
...(err.carrierAgentId ? { carrier_agent_id: err.carrierAgentId } : {}),
|
|
952
1092
|
recovery: "register_agent → export BOTBUDDY_SESSION_ID=<new session_id>",
|
|
953
1093
|
}, opts.agentProfile, { sessionTenant: opts.agentProfile?.tenant ?? sessionTenant ?? null, agentId: null }));
|
|
1094
|
+
await reportSetupError(opts, { error: err.errorCode, exitCode: EXIT.AUTH, conditions });
|
|
954
1095
|
process.exit(EXIT.AUTH);
|
|
955
1096
|
}
|
|
956
1097
|
const error = typedProfileError(err.errorCode);
|
|
957
1098
|
process.stderr.write(`botbuddy wait: profile authentication failed (${error}); run ${profileCredentialRecovery(opts.agentProfile)}\n`);
|
|
958
1099
|
emitReceipt(profileErrorReceipt(opts.agentProfile, error));
|
|
1100
|
+
await reportSetupError(opts, { error, exitCode: EXIT.AUTH, conditions });
|
|
959
1101
|
process.exit(EXIT.AUTH);
|
|
960
1102
|
}
|
|
961
1103
|
if (err && err.cap) {
|