@botbuddy/cli 1.45.6 → 1.45.7

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbuddy/cli",
3
- "version": "1.45.6",
3
+ "version": "1.45.7",
4
4
  "description": "BotBuddy — Swarm coordination CLI for multi-agent workflows",
5
5
  "type": "module",
6
6
  "bin": {
Binary file
package/src/stack.mjs CHANGED
@@ -33,8 +33,7 @@ import { callToolJson, CALL_AUTH_ERROR_CODES } from "./api.mjs";
33
33
  import { SERVER_URL, getConfig } from "./config.mjs";
34
34
  import { resolveOwnerToken, resolveAgentKey } from "./cli-credentials.mjs";
35
35
  import { AGENT_KEY_RE, readAgentSessionTokenEnv } from "./agent-key.mjs";
36
- import { resolveAgentSessionCredential } from "./agent-session.mjs";
37
- import { clearAgentState } from "./agent-state.mjs";
36
+ import { resolveAgentSessionCredential, selfHealAgentSession } from "./agent-session.mjs";
38
37
  import { runDockerCommand, runDockerWorkflow, ADMITTED_DOCKER_VALIDATIONS } from "./docker-hygiene.mjs";
39
38
  import { acquireStackLock, lockPathForProject, projectIdFromConfig } from "./stack-file-lock.mjs";
40
39
  import { assertLeasedStackIsolated, leasedStackIdentity, materializeRunStackDir, portMapInConfig, refreshLeasedStackRetention, releaseLeasedStackClaim, reservePortBlocks } from "./stack-isolation.mjs";
@@ -828,7 +827,7 @@ export async function stackAuthHeader({
828
827
  // and silently authenticate as the OAuth/MCP-key principal, which is the exact
829
828
  // principal switch the catch above guards against.
830
829
  if (typeof credential.token === "string" && AGENT_KEY_RE.test(credential.token)) {
831
- return withSource({ "x-agent-api-key": credential.token }, credential.source);
830
+ return withSource({ "x-agent-api-key": credential.token }, credential.source, credential.identity);
832
831
  }
833
832
  throw Object.assign(new Error(`resolved agent session token has an unexpected shape (source: ${credential.source ?? "unknown"}); re-run \`bb doctor --fix\``), { code: "session_token_malformed" });
834
833
  }
@@ -848,37 +847,39 @@ export async function stackAuthHeader({
848
847
  }
849
848
 
850
849
  // BOT-1750 (Codex R7 P2): carry the credential SOURCE on the returned headers as a
851
- // NON-enumerable property, so callers can clear+self-heal+retry once on a
850
+ // NON-enumerable property, so callers can renew+retry once on a
852
851
  // server-rejected cached session (parity with run/test/pw/wait) without the source
853
852
  // ever leaking into the spread fetch headers.
854
- function withSource(headers, source) {
853
+ function withSource(headers, source, identity = null) {
855
854
  Object.defineProperty(headers, "__source", { value: source ?? null, enumerable: false });
855
+ Object.defineProperty(headers, "__identity", { value: identity, enumerable: false });
856
856
  return headers;
857
857
  }
858
858
 
859
859
  /**
860
860
  * BOT-1750 (Codex R7 P2): a locally-fresh cache whose token was revoked server-side
861
861
  * (e.g. a concurrent re-registration rotated the session) is rejected at the lease
862
- * RPC. Mirror the other consumers: when the FIRST auth came from the per-worktree
863
- * cache and the RPC returns an auth rejection, clear the cache and self-heal once,
864
- * then let the caller retry with the fresh headers. Returns the fresh headers, or
865
- * null when no retry applies (not cache-sourced, or the refresh could not authenticate).
862
+ * RPC. Renew once without deleting the cache or losing lease ownership identity.
863
+ * The shared lock re-reads and verifies the cache; a concurrent renewal wins.
864
+ * Failure leaves the existing cache untouched and propagates its typed error.
866
865
  */
867
866
  export async function refreshRejectedCacheAuth(auth, {
868
- cwd = process.cwd(), authProvider = stackAuthHeader, clear = clearAgentState,
867
+ cwd = process.cwd(), renew = selfHealAgentSession,
869
868
  } = {}) {
870
869
  if (auth?.__source !== "cache") return null;
871
- await clear(cwd);
872
- const fresh = await authProvider();
873
- // BOT-1750 (Codex R8 P2): retry ONLY with a genuinely self-healed session. If the
874
- // client key or binding vanished after clearing the cache, authProvider falls
875
- // through to the OAuth/MCP operator credential — a DIFFERENT principal whose token
876
- // also differs, so a bare "different token" check would retry as the wrong agent.
877
- // Require source "minted" AND a well-formed, changed session token.
878
- if (fresh?.__source !== "minted") return null;
879
- const token = fresh["x-agent-api-key"];
870
+ return renewStackSession(auth, { cwd, renew });
871
+ }
872
+
873
+ async function renewStackSession(auth, { cwd, renew }) {
874
+ const credential = await renew({ cwd, force: true,
875
+ rejectedCredential: { token: auth["x-agent-api-key"], identity: auth.__identity } });
876
+ // BOT-1750 (Codex R8 P2): the renewal adapter must never yield an operator
877
+ // principal. "cache" is permitted
878
+ // only for a newer same-identity session verified inside the renewal lock.
879
+ if (credential?.source !== "minted" && credential?.source !== "cache") return null;
880
+ const token = credential.token;
880
881
  if (typeof token !== "string" || !AGENT_KEY_RE.test(token) || token === auth["x-agent-api-key"]) return null;
881
- return fresh;
882
+ return withSource({ "x-agent-api-key": token }, credential.source, credential.identity);
882
883
  }
883
884
 
884
885
  /**
@@ -886,8 +887,8 @@ export async function refreshRejectedCacheAuth(auth, {
886
887
  * server-rejected cached session ONCE — the owner-scoped management commands
887
888
  * (`status`/`touch`/`done`) need the same recovery as the lease-creating paths, or a
888
889
  * revoked-but-locally-fresh cache leaves an active lease unreleasable. `init()`
889
- * resolves auth; `call()` runs the RPC and, on the FIRST auth rejection, clears +
890
- * self-heals + retries with the fresh credential (every later `call` reuses it).
890
+ * resolves auth; `call()` runs the RPC and, on the FIRST auth rejection, renews +
891
+ * retries with the fresh credential (every later `call` reuses it).
891
892
  */
892
893
  export function retryingStackCall({
893
894
  callTool = callToolJson, authProvider = stackAuthHeader,
@@ -1362,25 +1363,18 @@ function abortableSleep(ms, signal) {
1362
1363
  * BOT-2099: re-establish the credential a parked lease waits with. On a transport failure
1363
1364
  * the credential is fine — keep it. On an auth rejection, first re-read (another process in
1364
1365
  * this worktree may already have re-minted, and minting again would supersede it), then
1365
- * clear the per-worktree cache and self-heal once. Only a well-formed agent SESSION token
1366
+ * renew under the shared cache lock without removing recovery identity. Only a well-formed agent SESSION token
1366
1367
  * replaces a session credential: the operator fallback is a different principal that
1367
1368
  * cannot see the lease (BOT-1750 R8), so it is never adopted. Throws the mint error, which
1368
1369
  * the park treats as one more failed attempt.
1369
1370
  */
1370
1371
  export async function reauthenticateParkedLease(current, kind, {
1371
- cwd = process.cwd(), authProvider = stackAuthHeader, clear = clearAgentState,
1372
+ cwd = process.cwd(), renew = selfHealAgentSession,
1372
1373
  } = {}) {
1373
1374
  if (kind !== "auth") return current;
1374
1375
  const source = current?.__source ?? null;
1375
1376
  if (source !== "cache" && source !== "minted") return current;
1376
- const isFreshSession = (h) => (h?.__source === "cache" || h?.__source === "minted")
1377
- && typeof h["x-agent-api-key"] === "string" && AGENT_KEY_RE.test(h["x-agent-api-key"])
1378
- && h["x-agent-api-key"] !== current["x-agent-api-key"];
1379
- const reread = await authProvider();
1380
- if (isFreshSession(reread)) return reread;
1381
- await clear(cwd);
1382
- const minted = await authProvider();
1383
- return isFreshSession(minted) ? minted : current;
1377
+ return (await renewStackSession(current, { cwd, renew })) ?? current;
1384
1378
  }
1385
1379
 
1386
1380
  /**
@@ -2482,7 +2476,7 @@ export async function cmdDone(leaseId, opts, {
2482
2476
  } = {}) {
2483
2477
  // BOT-1750 (Codex R10 P2): `done` must recover a server-rejected cached session on
2484
2478
  // its own — otherwise an active lease can't be released until the cache is manually
2485
- // repaired. The retrying caller clears+self-heals+retries the FIRST auth rejection.
2479
+ // repaired. The retrying caller renews+retries the FIRST auth rejection.
2486
2480
  const rpc = retryingStackCall({ callTool, authProvider, refresh: refreshAuth });
2487
2481
  const auth = await rpc.init();
2488
2482
  if (!auth) return emitResult(buildReceipt({ command: "done", outcome: "error", lease_id: leaseId, error: "not authenticated — run `botbuddy login`" }), opts, EXIT.AUTH);
@@ -2839,7 +2833,7 @@ async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
2839
2833
  // BOT-1750 (Codex R9/R11 P2): `stack run` is a lease-creating entry point AND a
2840
2834
  // long-lived one (queue → provision → child → heartbeat → cleanup). A cached token
2841
2835
  // can be rotated server-side at ANY of those points, so EVERY RPC — not just the
2842
- // initial request — must be able to clear + self-heal + retry once. Route them all
2836
+ // initial request — must be able to renew + retry once. Route them all
2843
2837
  // through one retryingStackCall so a single self-heal re-authenticates the rest
2844
2838
  // (heartbeat, get, cleanup release), instead of fencing a healthy child or leaking
2845
2839
  // an unreleasable lease. Retry only when we own the credential or a test provides an
@@ -3110,7 +3104,7 @@ async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
3110
3104
  ...(bundle?.included?.length ? { bundle_included: bundle.included } : {}),
3111
3105
  ...(opts.callbackPort != null ? { callback_port: opts.callbackPort } : {}),
3112
3106
  };
3113
- // The retrying caller (api → rpc.call) transparently clears+self-heals+retries the
3107
+ // The retrying caller (api → rpc.call) transparently renews+retries the
3114
3108
  // FIRST auth rejection among any of request/get/touch/release.
3115
3109
  // BOT-2024: a stale-capacity refusal prints phase-aware guidance; with the opt-in
3116
3110
  // --wait-capacity a TRANSIENT one parks on the server's capacity wait and re-admits once.