@botbuddy/cli 1.45.7 → 1.46.1
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/run.mjs +77 -35
package/package.json
CHANGED
package/src/run.mjs
CHANGED
|
@@ -15,7 +15,9 @@ import { fileURLToPath } from "node:url";
|
|
|
15
15
|
import { callToolJson } from "./api.mjs";
|
|
16
16
|
import { parseLaneEvents, laneCaseCounts, flushLaneCases, buildLaneSummary, laneSummaryFilename } from "./test-lane-events.mjs";
|
|
17
17
|
import { AGENT_KEY_RE, readAgentSessionTokenEnv, legacyAgentAliasHint } from "./agent-key.mjs";
|
|
18
|
-
import {
|
|
18
|
+
import { hasAgentSessionRuntime, isRejectedCachedMcpSession, resolveAgentSessionCredential, selfHealAgentSession } from "./agent-session.mjs";
|
|
19
|
+
import { isAgentStateFresh, readAgentState, worktreeRoot } from "./agent-state.mjs";
|
|
20
|
+
import { readAgentBinding } from "./wait-profile.mjs";
|
|
19
21
|
import { createTelemetryOutbox } from "./telemetry-outbox.mjs";
|
|
20
22
|
import { deliverExecutionEvent } from "./telemetry-delivery.mjs";
|
|
21
23
|
import { attestTelemetryCredential, loadTelemetryIdentity } from "./telemetry-config.mjs";
|
|
@@ -231,49 +233,82 @@ function workerInvocation(context) {
|
|
|
231
233
|
return [process.execPath, [bin, "run", "--worker", context]];
|
|
232
234
|
}
|
|
233
235
|
|
|
236
|
+
function sessionFailure(code) { return Object.assign(new Error(code), { code }); }
|
|
237
|
+
|
|
238
|
+
// A selected ID is metadata, not a credential. Require positive private-cache
|
|
239
|
+
// authority before resolving it; never let the API's ambient owner key stand in.
|
|
240
|
+
async function resolveRunCredential({ cwd, env, opts, resolveCredential }) {
|
|
241
|
+
const cached = await readAgentState(cwd);
|
|
242
|
+
const explicitId = opts.sessionId;
|
|
243
|
+
const validateBinding = async (identity) => {
|
|
244
|
+
const bound = await readAgentBinding(cwd);
|
|
245
|
+
if (!bound?.tenant || identity?.tenant !== bound.tenant || !identity?.worktree
|
|
246
|
+
|| await worktreeRoot(identity.worktree) !== await worktreeRoot(cwd)) {
|
|
247
|
+
throw sessionFailure("run_session_binding_mismatch");
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
if (explicitId && !cached) throw sessionFailure("run_session_authority_missing");
|
|
251
|
+
if (cached) {
|
|
252
|
+
if (explicitId && cached.session_id !== explicitId) throw sessionFailure("run_session_mismatch");
|
|
253
|
+
await validateBinding(cached);
|
|
254
|
+
if (explicitId && !isAgentStateFresh(cached, new Date())) throw sessionFailure("run_session_expired");
|
|
255
|
+
} else if (await hasAgentSessionRuntime({ cwd })) {
|
|
256
|
+
throw sessionFailure("run_session_authority_missing");
|
|
257
|
+
}
|
|
258
|
+
const credential = await (resolveCredential ?? resolveAgentSessionCredential)({ cwd, env,
|
|
259
|
+
sessionId: explicitId, flagToken: null });
|
|
260
|
+
// The resolver reads again. A concurrent replacement must satisfy the same
|
|
261
|
+
// binding and explicit ID rather than silently adopting unrelated authority.
|
|
262
|
+
if (credential.source === "cache") {
|
|
263
|
+
await validateBinding(credential.identity);
|
|
264
|
+
if (explicitId && credential.identity?.sessionId !== explicitId) throw sessionFailure("run_session_mismatch");
|
|
265
|
+
}
|
|
266
|
+
return credential;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function rejectedSessionReceipt(error, runId) {
|
|
270
|
+
const code = error?.code ?? "agent_session_unavailable";
|
|
271
|
+
const exitCode = ["session_mint_failed", "session_mint_invalid"].includes(code) ? EXIT.BACKEND : EXIT.AUTH;
|
|
272
|
+
return { exitCode, receipt: { schema_version: RUN_SCHEMA_VERSION, outcome: "rejected",
|
|
273
|
+
...(runId ? { run_id: runId } : {}), error: code,
|
|
274
|
+
recovery: code === "client_key_required"
|
|
275
|
+
? "run `bb login` to install the durable client key, then retry"
|
|
276
|
+
: "run `bb doctor --fix` and select the current worktree session, then retry",
|
|
277
|
+
exit_code: exitCode } };
|
|
278
|
+
}
|
|
279
|
+
|
|
234
280
|
export async function launchRun(argv, {
|
|
235
281
|
cwd = process.cwd(), spawnImpl = spawn, call = null, childEnv = null, testRun = null,
|
|
236
|
-
resolveCredential = null, env = process.env,
|
|
282
|
+
resolveCredential = null, renewCredential = selfHealAgentSession, env = process.env,
|
|
237
283
|
} = {}) {
|
|
238
|
-
|
|
239
|
-
let parsed = parseRunArgs(effectiveArgv, env);
|
|
284
|
+
const parsed = parseRunArgs(argv, env);
|
|
240
285
|
let managedCredential = null;
|
|
241
286
|
// A local test double owns its credential boundary; production invocations
|
|
242
287
|
// self-heal when the session credential was not supplied by flag or env.
|
|
243
288
|
// Keep all other usage failures local and deterministic before minting.
|
|
244
289
|
const onlyMissingIdentity = parsed.errors.length === 1 && parsed.errors[0].includes("session-id");
|
|
245
|
-
if (!parsed.opts.foreground && !parsed.opts.
|
|
246
|
-
&&
|
|
290
|
+
if (!parsed.opts.foreground && !parsed.opts.sessionToken
|
|
291
|
+
&& (parsed.errors.length === 0 || onlyMissingIdentity)
|
|
292
|
+
&& (resolveCredential || call === null || call === callToolJson)) {
|
|
247
293
|
try {
|
|
248
|
-
const credential = await (
|
|
294
|
+
const credential = await resolveRunCredential({ cwd, env, opts: parsed.opts, resolveCredential });
|
|
249
295
|
managedCredential = credential;
|
|
250
|
-
|
|
251
|
-
|
|
296
|
+
parsed.opts.sessionToken = credential.token;
|
|
297
|
+
if (!SESSION_TOKEN_RE.test(credential.token)) throw sessionFailure("session_mint_invalid");
|
|
298
|
+
if (onlyMissingIdentity) parsed.errors.length = 0;
|
|
252
299
|
} catch (error) {
|
|
253
|
-
return
|
|
254
|
-
exitCode: EXIT.AUTH,
|
|
255
|
-
receipt: {
|
|
256
|
-
schema_version: RUN_SCHEMA_VERSION,
|
|
257
|
-
outcome: "rejected",
|
|
258
|
-
error: error?.code ?? "agent_session_unavailable",
|
|
259
|
-
recovery: error?.code === "client_key_required"
|
|
260
|
-
? "run `bb login` to install the durable client key, then retry"
|
|
261
|
-
: "run `bb doctor --fix` to repair agent authentication, then retry",
|
|
262
|
-
exit_code: EXIT.AUTH,
|
|
263
|
-
},
|
|
264
|
-
};
|
|
300
|
+
return rejectedSessionReceipt(error);
|
|
265
301
|
}
|
|
266
302
|
}
|
|
267
303
|
const { opts, command, errors } = parsed;
|
|
268
304
|
if (errors.length) return { exitCode: EXIT.INVALID, receipt: { schema_version: RUN_SCHEMA_VERSION, outcome: "rejected", errors, exit_code: EXIT.INVALID } };
|
|
269
305
|
// BOT-1572 (AC-8): when a session token is present it is the credential — pin it
|
|
270
306
|
// so register_command authenticates as the session's agent and derives session_id
|
|
271
|
-
// from the token.
|
|
272
|
-
const effectiveCall = call ?? (
|
|
273
|
-
?
|
|
274
|
-
: callToolJson);
|
|
307
|
+
// from the token. Read the current token on each call, including after renewal.
|
|
308
|
+
const effectiveCall = (t, a) => (call ?? callToolJson)(t, a,
|
|
309
|
+
opts.sessionToken ? { auth: { "x-agent-api-key": opts.sessionToken } } : {});
|
|
275
310
|
const runId = randomUUID();
|
|
276
|
-
const registerArgs = {
|
|
311
|
+
const registerArgs = () => ({
|
|
277
312
|
// Omit session_id entirely when only a token is present — the backend derives
|
|
278
313
|
// it. A plain id is still sent (and honoured) when supplied.
|
|
279
314
|
...(opts.sessionId ? { session_id: opts.sessionId } : {}),
|
|
@@ -281,18 +316,22 @@ export async function launchRun(argv, {
|
|
|
281
316
|
command_hash: commandHash(command), environment_hash: environmentHash(cwd, opts.environment),
|
|
282
317
|
environment: opts.environment, command_kind: opts.kind, expected_duration_seconds: opts.expectedDuration,
|
|
283
318
|
rerun_reason: opts.rerunReason,
|
|
284
|
-
};
|
|
285
|
-
let registration = await effectiveCall("register_command", registerArgs);
|
|
319
|
+
});
|
|
320
|
+
let registration = await effectiveCall("register_command", registerArgs());
|
|
286
321
|
// A cached session can be revoked server-side before its local expiry. The
|
|
287
322
|
// managed cache is ours to replace once; explicit flag/env credentials belong
|
|
288
323
|
// to the caller and are never silently rotated.
|
|
289
324
|
if (isRejectedCachedMcpSession(registration, managedCredential?.source, 0)) {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
325
|
+
try {
|
|
326
|
+
const credential = await renewCredential({ cwd, env, force: true, rejectedCredential: {
|
|
327
|
+
token: managedCredential.token, identity: managedCredential.identity,
|
|
328
|
+
} });
|
|
329
|
+
managedCredential = credential;
|
|
330
|
+
opts.sessionToken = credential.token;
|
|
331
|
+
opts.sessionId = credential.identity?.sessionId ?? credential.sessionId;
|
|
332
|
+
if (!SESSION_TOKEN_RE.test(opts.sessionToken) || !opts.sessionId) throw sessionFailure("session_mint_invalid");
|
|
333
|
+
registration = await effectiveCall("register_command", registerArgs());
|
|
334
|
+
} catch (error) { return rejectedSessionReceipt(error, runId); }
|
|
296
335
|
}
|
|
297
336
|
if (!registration.ok || registration.isError || !registration.data?.accepted || !registration.data?.run_credential) {
|
|
298
337
|
return { exitCode: EXIT.BACKEND, receipt: { schema_version: RUN_SCHEMA_VERSION, outcome: "rejected", run_id: runId, error: registration.data?.error ?? registration.error ?? "durable_owner_not_established", exit_code: EXIT.BACKEND } };
|
|
@@ -310,7 +349,10 @@ export async function launchRun(argv, {
|
|
|
310
349
|
await effectiveCall("update_command_run", { run_id: runId, run_credential: registration.data.run_credential, status: "owner_lost", invalidated_reason: `launcher_failed:${error instanceof Error ? error.message : String(error)}` });
|
|
311
350
|
return { exitCode: EXIT.INTERNAL, receipt: { schema_version: RUN_SCHEMA_VERSION, outcome: "owner_lost", run_id: runId, exit_code: EXIT.INTERNAL } };
|
|
312
351
|
}
|
|
313
|
-
|
|
352
|
+
const knownSession = opts.sessionId ?? managedCredential?.identity?.sessionId ?? managedCredential?.sessionId;
|
|
353
|
+
return { exitCode: EXIT.OK, receipt: { schema_version: RUN_SCHEMA_VERSION, outcome: "launched", run_id: runId,
|
|
354
|
+
...(knownSession ? { session_id: knownSession } : { session_binding: "server_derived" }),
|
|
355
|
+
durable_owner: "detached_worker", receipt_path: receipt, reentry: "update_command_run publishes the terminal callback for this run_id", exit_code: EXIT.OK } };
|
|
314
356
|
}
|
|
315
357
|
|
|
316
358
|
export async function runWorker(contextFile, { spawnImpl = spawn, call = callToolJson } = {}) {
|