@botbuddy/cli 1.63.17 → 1.63.19

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.
@@ -1,7 +1,7 @@
1
1
  {
2
- "release_version": "1.63.17",
3
- "source_commit": "91a6f2a341b346544416c7e94f4c893462e64b9f",
4
- "source_sha256": "fa5d89b27b76755835a5f2e9542c944b900c6dfb7c3198d2f8a222e3c98ce628",
2
+ "release_version": "1.63.19",
3
+ "source_commit": "e026917ba4620d7590111b2bdc26bdfcbbe44bb6",
4
+ "source_sha256": "aa4da801723f3b6afa813cdcb7288eb47943ba73a64bdb1a583215eb7eaf107e",
5
5
  "target": "darwin-arm64",
6
- "sha256": "0c4dc99976f874be35bbb079bceb1347be7719ffb0efd0f59337fcec3e184b1d"
6
+ "sha256": "944be3c82acc2979da23e704f21002889989aa54816ee812c25de52f8bb89054"
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
- "release_version": "1.63.17",
3
- "source_commit": "91a6f2a341b346544416c7e94f4c893462e64b9f",
4
- "source_sha256": "fa5d89b27b76755835a5f2e9542c944b900c6dfb7c3198d2f8a222e3c98ce628",
2
+ "release_version": "1.63.19",
3
+ "source_commit": "e026917ba4620d7590111b2bdc26bdfcbbe44bb6",
4
+ "source_sha256": "aa4da801723f3b6afa813cdcb7288eb47943ba73a64bdb1a583215eb7eaf107e",
5
5
  "target": "linux-x64",
6
- "sha256": "f2e0075e06393bb5830e70fd2cb11592195b484301c3bc37bf2f899dec91b299"
6
+ "sha256": "f2157250a4cc11eb9e9b6b21b3b08c14483f780889d8f1397a5594c48f8fbc0b"
7
7
  }
@@ -1,21 +1,21 @@
1
1
  {
2
2
  "schema_version": 1,
3
- "release_version": "1.63.17",
4
- "source_commit": "91a6f2a341b346544416c7e94f4c893462e64b9f",
5
- "source_sha256": "fa5d89b27b76755835a5f2e9542c944b900c6dfb7c3198d2f8a222e3c98ce628",
3
+ "release_version": "1.63.19",
4
+ "source_commit": "e026917ba4620d7590111b2bdc26bdfcbbe44bb6",
5
+ "source_sha256": "aa4da801723f3b6afa813cdcb7288eb47943ba73a64bdb1a583215eb7eaf107e",
6
6
  "targets": {
7
7
  "darwin-arm64": {
8
8
  "goos": "darwin",
9
9
  "goarch": "arm64",
10
10
  "binary": "daemon/bin/darwin-arm64/botbuddy-daemon",
11
- "sha256": "0c4dc99976f874be35bbb079bceb1347be7719ffb0efd0f59337fcec3e184b1d",
11
+ "sha256": "944be3c82acc2979da23e704f21002889989aa54816ee812c25de52f8bb89054",
12
12
  "mode": "0755"
13
13
  },
14
14
  "linux-x64": {
15
15
  "goos": "linux",
16
16
  "goarch": "amd64",
17
17
  "binary": "daemon/bin/linux-x64/botbuddy-daemon",
18
- "sha256": "f2e0075e06393bb5830e70fd2cb11592195b484301c3bc37bf2f899dec91b299",
18
+ "sha256": "f2157250a4cc11eb9e9b6b21b3b08c14483f780889d8f1397a5594c48f8fbc0b",
19
19
  "mode": "0755"
20
20
  }
21
21
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbuddy/cli",
3
- "version": "1.63.17",
3
+ "version": "1.63.19",
4
4
  "description": "BotBuddy \u2014 Swarm coordination CLI for multi-agent workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -791,9 +791,12 @@ export function stableRunStackReusable(worktreeRoot, dirName, { read = readFileS
791
791
  * generated links name the listener of the invocation that started that stack, so
792
792
  * refuse any unreleased marker for this slot before deriving a new bundle or ports.
793
793
  */
794
- export function assertFreshRunStackAvailable(worktreeRoot, slot, { read = readFileSync, readdir = readdirSync, lstat = lstatSync } = {}) {
794
+ export function assertFreshRunStackAvailable(worktreeRoot, slot, { read = readFileSync, readdir = readdirSync, lstat = lstatSync, liveOwnerOnly = false } = {}) {
795
795
  const refuse = () => {
796
- const error = new Error("requester origin cannot resume a retained slot context; inspect its owning lease with `bb stack status <lease-id>` and use `bb stack done <lease-id> --reap` only after that owner finishes, then confirm reaped before a fresh run");
796
+ const error = new Error(liveOwnerOnly
797
+ // A stable run refuses only a live owner: never suggest tearing its batch down.
798
+ ? "another `bb stack run` on this machine is still using this slot's stack; wait for it to finish, then re-run"
799
+ : "requester origin cannot resume a retained slot context; inspect its owning lease with `bb stack status <lease-id>` and use `bb stack done <lease-id> --reap` only after that owner finishes, then confirm reaped before a fresh run");
797
800
  error.code = "REQUESTER_ORIGIN_RETAINED_CONTEXT";
798
801
  throw error;
799
802
  };
@@ -809,16 +812,52 @@ export function assertFreshRunStackAvailable(worktreeRoot, slot, { read = readFi
809
812
  let marker;
810
813
  try { marker = JSON.parse(read(path, "utf8")); } catch { refuse(); }
811
814
  if (!marker || typeof marker.slot !== "string") refuse();
812
- if (marker.slot !== slot || marker.released === true) continue;
815
+ // BOT-2788: an idle-retained context is resumable. Its lease went idle cleanly, so no
816
+ // batch is using it; the control plane re-adopts it only for the exact remote variant
817
+ // (same logical origin) and reprovisions it through the canonical reap otherwise.
818
+ if (marker.slot !== slot || marker.released === true || marker.idle_retained === true) continue;
813
819
  // An expired context whose owner is gone is what materialization GC collects anyway; the
814
820
  // control plane still refuses a new remote lease while any lease on the slot is unreaped.
815
821
  const ownerAlive = Number.isInteger(marker.owner_pid) && marker.owner_pid > 0 &&
816
822
  (!marker.host || marker.host === hostname()) && pidIsAlive(marker.owner_pid);
817
823
  const expired = marker.idle_retained !== true && Number.isFinite(marker.retain_until) && Date.now() >= marker.retain_until;
818
- if (!expired || ownerAlive) refuse();
824
+ // BOT-2788: a stable run (liveOwnerOnly) refuses only while the owner may still run.
825
+ if (ownerAlive || (!liveOwnerOnly && !expired)) refuse();
819
826
  }
820
827
  }
821
828
 
829
+ const REQUESTER_ORIGIN_RE = /^http:\/\/127\.0\.0\.1:([0-9]{4,5})$/;
830
+ /**
831
+ * BOT-2788: the loopback port of the logical requester origin an IDLE stack in this stable
832
+ * directory generated its links for, or null. Only a cleanly idled context on this same
833
+ * machine qualifies; anything else starts a fresh origin (the control plane then reprovisions
834
+ * any idle stack whose remote variant differs).
835
+ */
836
+ export function idleRequesterOriginPort(worktreeRoot, dirName, { read = readFileSync, host = hostname } = {}) {
837
+ if (typeof dirName !== "string" || !dirName) return null;
838
+ const marker = readStackMarker(join(worktreeRoot, LEASED_STACKS_DIR, dirName), read);
839
+ if (!marker || marker.idle_retained !== true || marker.released === true) return null;
840
+ if (typeof marker.host !== "string" || marker.host !== host()) return null;
841
+ const match = REQUESTER_ORIGIN_RE.exec(typeof marker.requester_origin === "string" ? marker.requester_origin : "");
842
+ const port = match ? Number(match[1]) : NaN;
843
+ return Number.isInteger(port) && port >= 1024 && port <= 65535 ? port : null;
844
+ }
845
+
846
+ /** BOT-2788: does this stable directory hold a cleanly idled stack (its lease went idle)? */
847
+ export function stableRunStackIdle(worktreeRoot, dirName, { read = readFileSync } = {}) {
848
+ if (typeof dirName !== "string" || !dirName) return false;
849
+ const marker = readStackMarker(join(worktreeRoot, LEASED_STACKS_DIR, dirName), read);
850
+ return marker?.idle_retained === true && marker.released !== true;
851
+ }
852
+
853
+ /** BOT-2788: record the logical requester origin this stack's remote variant names. */
854
+ export function recordRequesterOrigin(stackDir, origin, { read = readFileSync, write = writeFileSync, rename = renameSync } = {}) {
855
+ if (!REQUESTER_ORIGIN_RE.test(origin ?? "")) throw new Error("requester origin refused");
856
+ const marker = readStackMarker(stackDir, read);
857
+ if (!marker) throw new Error("stack marker unavailable");
858
+ atomicWrite(join(stackDir, LEASED_STACK_MARKER), JSON.stringify({ ...marker, requester_origin: origin }), { write, rename });
859
+ }
860
+
822
861
  /** BOT-1803: record a PROVEN reap on the stack's marker, so the next run may reuse the directory. */
823
862
  export function markLeasedStackReleased(stackDir, { read = readFileSync, write = writeFileSync, rename = renameSync } = {}) {
824
863
  const marker = readStackMarker(stackDir, read);
@@ -1,6 +1,6 @@
1
1
  // BOT-2780: parse and roundtrip the private derived config before bundling.
2
2
  // The vendored, hash-pinned smol-toml 1.9.1 parser (src/vendor/smol-toml) is loaded only
3
- // for the beta requester-origin feature; help and legacy paths never import it.
3
+ // for requester-origin runs (BOT-2780/BOT-2788); help and legacy paths never import it.
4
4
  import {isDeepStrictEqual} from 'node:util';
5
5
  const fail=(code,message)=>{const error=Error(message);error.code=code;throw error};
6
6
  let parser;
package/src/stack.mjs CHANGED
@@ -43,7 +43,7 @@ import { AGENT_KEY_RE, readAgentSessionTokenEnv } from "./agent-key.mjs";
43
43
  import { resolveAgentSessionCredential, selfHealAgentSession } from "./agent-session.mjs";
44
44
  import { runDockerCommand, runDockerWorkflow, ADMITTED_DOCKER_VALIDATIONS } from "./docker-hygiene.mjs";
45
45
  import { acquireStackLock, lockPathForProject, projectIdFromConfig } from "./stack-file-lock.mjs";
46
- import { LEASED_STACK_IDLE_RETENTION_SECS, assertFreshRunStackAvailable, recordAdmissionRequest, bindLeasedStackClaim, recoverLeasedStackClaim, hasLocalLeaseClaim, leasedClaimContext, assertLeasedStackIsolated, leasedStackClaimHeld, markLeasedStackReleased, stableRunStackReusable, leasedStackIdentity, materializeRunStackDir, portMapInConfig, refreshLeasedStackRetention, releaseLeasedStackClaim, reservePortBlocks } from "./stack-isolation.mjs";
46
+ import { LEASED_STACK_IDLE_RETENTION_SECS, assertFreshRunStackAvailable, idleRequesterOriginPort, recordRequesterOrigin, stableRunStackIdle, recordAdmissionRequest, bindLeasedStackClaim, recoverLeasedStackClaim, hasLocalLeaseClaim, leasedClaimContext, assertLeasedStackIsolated, leasedStackClaimHeld, markLeasedStackReleased, stableRunStackReusable, leasedStackIdentity, materializeRunStackDir, portMapInConfig, refreshLeasedStackRetention, releaseLeasedStackClaim, reservePortBlocks } from "./stack-isolation.mjs";
47
47
  import { reserveRequesterOrigin } from "./stack-requester-origin.mjs";
48
48
  import { identifiedAdmissionAck, identifiedAdmissionDelivery, identifiedAdmissionRecovery } from "./stack-identified-admission.mjs";
49
49
  import { patchRequesterAuthOrigin } from "./stack-requester-origin-config.mjs";
@@ -62,11 +62,20 @@ import { reportStackRunOutcome } from "./stack-run-outcome.mjs";
62
62
  export const STACK_SCHEMA_VERSION = 1;
63
63
 
64
64
  /**
65
- * BOT-2780: only `stack run --remote --execution-ring beta --reap` reserves a requester
66
- * origin and allocates through BOT-2499's identified admission. The origin cannot outlive
67
- * its CLI, so the invocation must own cleanup; stable/auto/local and idle runs are unchanged.
65
+ * BOT-2780 / BOT-2788: a `stack run` that may be served by another host reserves a requester
66
+ * origin and allocates through BOT-2499's identified admission. Stable (the default ring):
67
+ * auto or remote placement, with or without `--reap` (an idle stack is continued on the same
68
+ * logical origin by a later run); runs that pin a host, a prepared stack path, a fixed nonce
69
+ * or a co-located placement, or pass a callback port (BOT-2775), keep the legacy path
70
+ * unchanged. Beta: BOT-2780's `--remote --reap` gate, which refuses those contexts instead.
68
71
  */
69
- export const requesterOriginEnabled = (opts) => opts.execution === "remote" && opts.executionRing === "beta" && opts.reap === true;
72
+ export const requesterOriginEnabled = (opts) => {
73
+ if (opts.execution === "local" || opts.localOnly || opts.localExec) return false;
74
+ // Beta keeps BOT-2780's gate (remote + --reap); its idle/callback runs stay on BOT-2775's path.
75
+ if (opts.executionRing === "beta") return opts.execution === "remote" && opts.reap === true;
76
+ if (opts.execution != null && opts.execution !== "auto" && opts.execution !== "remote") return false;
77
+ return opts.host == null && opts.stackNonce == null && (opts.stackPath == null || opts.stackPath === "" || opts.stackPath === ".") && opts.callbackPort == null;
78
+ };
70
79
  // Exact replays of one identified request after an uncertain answer (lost response, 5xx,
71
80
  // ADMISSION_UNCERTAIN, pending delivery). The same request_id can never create a second lease.
72
81
  const ADMISSION_REPLAY_DELAYS_MS = [1000, 2000, 4000];
@@ -255,12 +264,17 @@ ${bold("run OPTIONS")}
255
264
  else a remote host with capacity; the bundle digest is sent up front.
256
265
  --execution-ring R Capacity rollout ring: stable (default) excludes beta daemon hosts.
257
266
  beta is explicit opt-in and requires \`stack run --remote\`.
258
- With --remote --execution-ring beta --reap (BOT-2780), run reserves a
259
- loopback requester origin first and writes it as the private stack copy's
260
- [auth] external_url, so generated auth links (/auth/v1/verify…) reach the
261
- stack unchanged even when the SSH API forward is remapped; the child gets
262
- it as connection.requester_origin. The lease is requested through the
263
- identified admission (exact replay after a lost response, no fallback).
267
+ Requester origin (BOT-2780/BOT-2788): a stable \`stack run\` that may be
268
+ served remotely (auto or --remote; no --host/--stack-path/--callback-port)
269
+ and --remote --execution-ring beta --reap reserve a loopback origin first;
270
+ only the remote bundle's config names it as [auth] external_url, so
271
+ generated auth links (/auth/v1/verify…) reach the stack unchanged even when
272
+ the SSH API forward is remapped; the child gets connection.requester_origin.
273
+ A co-located placement runs the canonical stack and closes the origin.
274
+ The next run re-reserves an idle stack's origin to reuse it; when that
275
+ port is busy the idle stack is reprovisioned, never retargeted. The lease
276
+ is requested through the identified admission (exact replay after a lost
277
+ response, no fallback).
264
278
  --callback-port N The caller runs a listener the stack calls back through
265
279
  host.docker.internal:N (e.g. 4600): sent as callback_port. On a stable or
266
280
  auto REMOTE lease run first probes the host with a throwaway reverse forward
@@ -899,7 +913,7 @@ async function claimProjectLock(projectId, lock, subject) {
899
913
  }
900
914
  }
901
915
 
902
- export async function resolveRunExecution(cwd, opts, { materialize = materializeRunStackDir, lock = acquireStackLock, identity = leasedStackIdentity, reserveBlocks = reservePortBlocks, releaseClaim = releaseLeasedStackClaim, claimHeld = leasedStackClaimHeld, stableReusable = stableRunStackReusable, markReleased = markLeasedStackReleased, portRegistryDir = null, warn = (line) => process.stderr.write(`${line}\n`) } = {}) {
916
+ export async function resolveRunExecution(cwd, opts, { materialize = materializeRunStackDir, lock = acquireStackLock, identity = leasedStackIdentity, reserveBlocks = reservePortBlocks, releaseClaim = releaseLeasedStackClaim, claimHeld = leasedStackClaimHeld, stableReusable = stableRunStackReusable, markReleased = markLeasedStackReleased, idleOriginPort = idleRequesterOriginPort, stableIdle = stableRunStackIdle, recordOrigin = recordRequesterOrigin, reserveOrigin = reserveRequesterOrigin, portRegistryDir = null, warn = (line) => process.stderr.write(`${line}\n`) } = {}) {
903
917
  const refuseRoot = (execution) => {
904
918
  if (execution.stackPath === ".") {
905
919
  throw new Error(
@@ -909,10 +923,12 @@ export async function resolveRunExecution(cwd, opts, { materialize = materialize
909
923
  }
910
924
  return execution;
911
925
  };
912
- // BOT-2780: the requester origin is only for a fresh CLI-managed stack context. A
913
- // prepared path, pinned host or fixed nonce could resume a stack whose generated links
914
- // already name another invocation's listener.
915
- const originRun = requesterOriginEnabled(opts);
926
+ // BOT-2780: the requester origin is only for a CLI-managed stack context. A prepared path,
927
+ // pinned host or fixed nonce could resume a stack whose generated links name another
928
+ // invocation's listener (a stable run with one of them is simply not an origin run).
929
+ // A stable origin run is the caller's choice to supply its admission identity (runStack
930
+ // always does); a beta run without one is refused below.
931
+ const originRun = requesterOriginEnabled(opts) && (opts.executionRing === "beta" || (UUID_RE.test(opts.admissionRequestId ?? "") && Boolean(opts.requesterMachineId)));
916
932
  if (originRun && (opts.host != null || opts.stackNonce != null || (opts.stackPath && opts.stackPath !== ".") ||
917
933
  !UUID_RE.test(opts.admissionRequestId ?? "") || !opts.requesterMachineId)) {
918
934
  const error = new Error("beta requester origins require a fresh CLI-managed stack context (no --host, --stack-path or fixed nonce)");
@@ -986,27 +1002,70 @@ export async function resolveRunExecution(cwd, opts, { materialize = materialize
986
1002
  const held = await claimProjectLock(projectId, lock, `this batch's isolated stack (${projectId})`);
987
1003
  let requesterOrigin = null;
988
1004
  let originContext = null;
1005
+ let originContinuation = null;
1006
+ let previousPort = null;
1007
+ // BOT-2788: this run resumes the directory of a cleanly idled stack (its block is bound to it).
1008
+ const resumedIdle = originRun && nonce == null && stableIdle(worktreeRoot, stableDir);
989
1009
  try {
990
- let configText;
991
1010
  if (originRun) {
992
- // BOT-2780: refuse a retained slot context, then reserve the listener BEFORE the
993
- // private config (and so the bundle digest) is derived. The source config is never
994
- // changed; only the private materialized copy names the reserved origin.
995
- assertFreshRunStackAvailable(worktreeRoot, opts.slot);
1011
+ // BOT-2780: refuse a live retained slot context, then reserve the listener BEFORE the
1012
+ // remote config variant (and so the bundle digest) is derived.
1013
+ // A stable run refuses only a context whose owner is still running; a dead owner's
1014
+ // unexpired claim is the control plane's to judge (it refuses any live slot lease).
1015
+ assertFreshRunStackAvailable(worktreeRoot, opts.slot, { liveOwnerOnly: opts.executionRing !== "beta" });
996
1016
  originContext = Object.freeze({ request_id: opts.admissionRequestId, worktree: worktreeRoot, slot: opts.slot, machine: opts.requesterMachineId });
997
- requesterOrigin = await reserveRequesterOrigin({ context: originContext });
998
- // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal -- canonical realpath of the current worktree; fixed own source config, never a request-supplied path.
999
- configText = await patchRequesterAuthOrigin(readFileSync(join(worktreeRoot, "supabase", "config.toml"), "utf8"), requesterOrigin.origin);
1017
+ // BOT-2788: an idle stack in this directory generated its links for a logical origin.
1018
+ // Re-reserve exactly that port, so the remote variant (and so the idle lease) is the
1019
+ // same. A busy port is never retargeted: a new origin makes a new variant, which the
1020
+ // control plane reprovisions through its canonical reap instead of re-adopting.
1021
+ previousPort = nonce == null ? idleOriginPort(worktreeRoot, stableDir) : null;
1022
+ if (previousPort != null) {
1023
+ try {
1024
+ requesterOrigin = await reserveOrigin({ context: originContext, port: previousPort });
1025
+ originContinuation = "previous";
1026
+ } catch (error) {
1027
+ if (error?.code !== "REQUESTER_ORIGIN_PORT_BUSY") throw error;
1028
+ originContinuation = "previous_port_busy";
1029
+ warn(`stack: the idle stack's requester origin 127.0.0.1:${previousPort} is in use on this machine; reserving a new origin (the idle stack is reprovisioned, never retargeted)`);
1030
+ }
1031
+ }
1032
+ if (!requesterOrigin) {
1033
+ requesterOrigin = await reserveOrigin({ context: originContext });
1034
+ originContinuation ??= "new";
1035
+ }
1036
+ // Validate the source config before anything is materialized: an explicit conflicting
1037
+ // auth issuer refuses a beta run, and keeps a stable run on the unchanged legacy path.
1038
+ try {
1039
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal -- canonical realpath of the current worktree; fixed own source config, never a request-supplied path.
1040
+ await patchRequesterAuthOrigin(readFileSync(join(worktreeRoot, "supabase", "config.toml"), "utf8"), requesterOrigin.origin);
1041
+ } catch (error) {
1042
+ if (error?.code !== "REQUESTER_ORIGIN_EXPLICIT_CONFLICT" || opts.executionRing === "beta") throw error;
1043
+ warn("stack: supabase/config.toml sets its own [auth] external_url, so this run keeps the legacy (non-requester-origin) path");
1044
+ await requesterOrigin.close();
1045
+ requesterOrigin = null;
1046
+ originContinuation = null;
1047
+ }
1000
1048
  }
1049
+ // The materialized stack stays canonical (a co-located placement serves it unchanged);
1050
+ // only the remote bundle's config variant names the reserved origin.
1001
1051
  const execution = refuseRoot(resolveStackPath(cwd, materialize(
1002
1052
  cwd,
1003
1053
  { slot: opts.slot, repo: opts.repo, ticket: opts.ticket, nonce, idleTtlSecs: opts.idleTtl ?? undefined },
1004
1054
  // BOT-2403: a functions directory another writer replaced under this stack is reported.
1005
- { ...(portRegistryDir ? { portRegistryDir } : {}), warn, ...(configText !== undefined ? { configText } : {}) },
1055
+ { ...(portRegistryDir ? { portRegistryDir } : {}), warn },
1006
1056
  )));
1057
+ let originConfigText = null;
1058
+ if (requesterOrigin) {
1059
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal -- this invocation's own materialized stack directory, never a request-supplied path.
1060
+ originConfigText = await patchRequesterAuthOrigin(readFileSync(join(stackDirFor(execution), "supabase", "config.toml"), "utf8"), requesterOrigin.origin);
1061
+ // Re-materializing rewrote the marker: keep the idle stack's own origin on it until this
1062
+ // run is admitted (runStack records the reserved one then, via recordOrigin below).
1063
+ if (previousPort != null) recordOrigin(stackDirFor(execution), `http://127.0.0.1:${previousPort}`);
1064
+ }
1007
1065
  return {
1008
1066
  ...execution,
1009
- ...(requesterOrigin ? { requesterOrigin, originContext, admissionRequestId: opts.admissionRequestId,
1067
+ ...(requesterOrigin ? { requesterOrigin, originContext, originConfigText, originContinuation, resumedIdle, admissionRequestId: opts.admissionRequestId,
1068
+ recordOrigin: () => recordOrigin(stackDirFor(execution), requesterOrigin.origin),
1010
1069
  recordAdmission: (requestId) => recordAdmissionRequest(stackDirFor(execution), requestId, portRegistryDir ? { registryDir: portRegistryDir } : undefined) } : {}),
1011
1070
  bindLease: (leaseId) => bindLeasedStackClaim(stackDirFor(execution), leaseId, portRegistryDir ? { registryDir: portRegistryDir } : undefined),
1012
1071
  // Release is called once the batch's signed reap is done, so hand the host
@@ -3492,6 +3551,8 @@ export async function recoverLocalLeaseClaim(lease, proof, { cwd = process.cwd()
3492
3551
  const SIGNAL_EXIT = Object.freeze({ SIGINT: 130, SIGTERM: 143, SIGHUP: 129 });
3493
3552
  /** BOT-1803: request answers meaning "this slot's idle lease is being reaped; re-request once it is". */
3494
3553
  const IDLE_REPROVISION_REASONS = Object.freeze({ IDLE_LEASE_REPROVISIONING: "digest_mismatch", LEASE_PREEMPTED: "preempted" });
3554
+ // BOT-2788: identified answers about the caller's OWN slot lease (no receipt was written).
3555
+ const IDENTIFIED_OWN_SLOT_ANSWERS = new Set([...Object.keys(IDLE_REPROVISION_REASONS), "SLOT_REAPING", "QUEUED_LEASE_VARIANT_MISMATCH", "EXISTING_SLOT_REFUSED"]);
3495
3556
  // BOT-2403: the re-adopted idle stack failed the functions-mount gate and was reaped.
3496
3557
  const READOPT_UNHEALTHY_REASON = "unhealthy_mount";
3497
3558
 
@@ -3949,7 +4010,10 @@ async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
3949
4010
  // BOT-2780: one identified admission key per invocation, held through replays.
3950
4011
  ...(requesterOriginEnabled(opts) ? { admissionRequestId: randomUUID(), requesterMachineId: hardwareUuid } : {}),
3951
4012
  });
3952
- if (requesterOriginEnabled(opts) && (!execution.requesterOrigin || !execution.admissionRequestId || !execution.originContext)) {
4013
+ // A beta run must hold its origin; a stable run whose repo pins its own auth issuer keeps
4014
+ // the legacy path (resolveRunExecution reports that), and any origin it holds is complete.
4015
+ if ((requesterOriginEnabled(opts) && opts.executionRing === "beta" && !execution.requesterOrigin) ||
4016
+ (execution.requesterOrigin && (!execution.admissionRequestId || !execution.originContext || typeof execution.originConfigText !== "string"))) {
3953
4017
  await closeOrigin();
3954
4018
  const error = new Error("requester origin context unavailable"); error.code = "REQUESTER_ORIGIN_CONTEXT_REFUSED"; throw error;
3955
4019
  }
@@ -3981,13 +4045,24 @@ async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
3981
4045
  let executionRequest = opts.execution ?? "auto";
3982
4046
  let bundle = null;
3983
4047
  const bundleInclude = opts.bundleInclude ?? [];
3984
- const buildRunBundle = ({ digestOnly = false } = {}) => {
4048
+ // BOT-2788: the remote variant differs from the materialized (canonical) stack only in
4049
+ // its private supabase/config.toml, which names the reserved requester origin.
4050
+ let originConfigPath = null;
4051
+ if (execution.requesterOrigin) {
4052
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal -- this invocation's own materialized stack directory, never a request-supplied path.
4053
+ try { originConfigPath = realpathSync(join(stackDirFor(execution), "supabase", "config.toml")); } catch { originConfigPath = null; }
4054
+ }
4055
+ const buildRunBundle = ({ digestOnly = false, remoteVariant = false } = {}) => {
3985
4056
  // BOT-2162: ship the worktree's BOT-2107 readiness declaration with the stack, so the
3986
4057
  // serving Helper runs the canonical bind-RPC probe for a remote lease too.
3987
4058
  const declaration = join(execution.worktreeRoot, ".botbuddy", "config.json");
3988
4059
  let hasDeclaration = false;
3989
4060
  try { hasDeclaration = existsSync(declaration) && lstatSync(declaration).isFile(); } catch { hasDeclaration = false; }
4061
+ if (remoteVariant && !originConfigPath) {
4062
+ const error = new Error("the requester-origin config variant has no materialized config to replace"); error.code = "REQUESTER_ORIGIN_DIGEST_UNAVAILABLE"; throw error;
4063
+ }
3990
4064
  return buildBundle(stackDirFor(execution), {
4065
+ ...(remoteVariant ? { readFile: (abs, ...rest) => abs === originConfigPath ? Buffer.from(execution.originConfigText, "utf8") : readFileSync(abs, ...rest) } : {}),
3991
4066
  // A digest that ships nothing has no upload cap (the archive is only hashed).
3992
4067
  maxBytes: digestOnly ? Number.MAX_SAFE_INTEGER : (opts.bundleMaxBytes ?? DEFAULT_BUNDLE_MAX_BYTES),
3993
4068
  ...(digestOnly ? { rawLimitBytes: Number.MAX_SAFE_INTEGER } : {}),
@@ -4003,12 +4078,15 @@ async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
4003
4078
  // an idle lease instead of handing back a stack whose content it cannot prove current.
4004
4079
  let stackDigest = null;
4005
4080
  let digestError = null;
4006
- if (executionRequest === "local") {
4081
+ // BOT-2788: an `auto` origin run keeps the canonical (co-located) digest, so local idle
4082
+ // re-adoption is unchanged; the control plane matches a remote idle stack by its exact
4083
+ // remote variant (bundle_sha256) instead. An explicit remote run's digest IS the variant.
4084
+ if (executionRequest === "local" || (execution.requesterOrigin && executionRequest !== "remote")) {
4007
4085
  try { stackDigest = buildRunBundle({ digestOnly: true })?.sha256 ?? null; } catch (error) { digestError = error; }
4008
4086
  }
4009
4087
  if (executionRequest !== "local") {
4010
4088
  try {
4011
- bundle = buildRunBundle();
4089
+ bundle = buildRunBundle({ remoteVariant: Boolean(execution.requesterOrigin) });
4012
4090
  for (const entry of bundle.included ?? []) {
4013
4091
  stderrLine(`stack: the bundle includes ${entry.path} (sha256 ${entry.sha256.slice(0, 12)}…; keys: ${entry.keys.join(", ") || "none"})`);
4014
4092
  }
@@ -4024,6 +4102,12 @@ async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
4024
4102
  // Codex #1077 P2: without a digest, `auto` could still pick a remote host and be
4025
4103
  // refused BUNDLE_REQUIRED instead of queueing here, so ask for this machine.
4026
4104
  executionRequest = "local";
4105
+ // BOT-2788: a co-located request needs no requester origin; it takes the unchanged
4106
+ // legacy path exactly as before (only a beta run, always remote, must keep its origin).
4107
+ if (execution.requesterOrigin) {
4108
+ await closeOrigin();
4109
+ for (const key of ["requesterOrigin", "originContext", "originConfigText", "originContinuation", "admissionRequestId", "recordAdmission", "recordOrigin"]) delete execution[key];
4110
+ }
4027
4111
  stderrLine(`${yellow("⚠")} stack: could not build the remote-lease bundle (${message}); requesting a co-located lease (execution local) instead.`);
4028
4112
  }
4029
4113
  }
@@ -4061,6 +4145,7 @@ async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
4061
4145
  // server-side; replays of this exact request can never create a second lease.
4062
4146
  let identifiedVerdict = null;
4063
4147
  let requestIdentified = null;
4148
+ let renewIdentifiedRequest = null;
4064
4149
  if (execution.admissionRequestId) {
4065
4150
  if (!stackDigest || !bundle?.sha256) {
4066
4151
  return { exitCode: EXIT.INTERNAL, outcome: "error", code: "REQUESTER_ORIGIN_DIGEST_UNAVAILABLE", error: "the requester-origin bundle digest could not be computed; no lease was requested" };
@@ -4079,6 +4164,13 @@ async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
4079
4164
  const settle = (code) => {
4080
4165
  identifiedVerdict = ownedAck ? { kind: "owned", code, ack: ownedAck } : { kind: "uncertain", code, ack: null };
4081
4166
  };
4167
+ // BOT-2788: a readopted key is consumed once its idle stack is reaped as unhealthy; the
4168
+ // fresh request after that reap is a new admission with its own recorded key.
4169
+ renewIdentifiedRequest = () => {
4170
+ leaseArgs.request_id = randomUUID();
4171
+ execution.recordAdmission?.(leaseArgs.request_id);
4172
+ replays = 0; mayHaveCommitted = false; ownedAck = null; identifiedVerdict = null;
4173
+ };
4082
4174
  // Returns a legacy-shaped answer: the decorated delivery for a ready ACK, a definite
4083
4175
  // refusal, or (with identifiedVerdict set) the original answer for the caller to retain.
4084
4176
  requestIdentified = async () => {
@@ -4099,22 +4191,39 @@ async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
4099
4191
  } else if (res.data?.success !== true) {
4100
4192
  const code = typeof res.data?.code === "string" ? res.data.code : "ADMISSION_UNCERTAIN";
4101
4193
  if (ADMISSION_COMMITTED_UNCERTAIN.has(code)) { settle(code); return res; }
4194
+ // BOT-2788: the server answers these only after its receipt probe found no admission
4195
+ // for this key, so they prove nothing was committed under it — even after a lost
4196
+ // response. The lease id names the caller's OWN slot lease, kept only so the run can
4197
+ // wait for its reap (or report it); it is never adopted or released from here.
4198
+ if (IDENTIFIED_OWN_SLOT_ANSWERS.has(code) && UUID_RE.test(res.data?.lease_id ?? "")) {
4199
+ return { ...res, data: { success: false, code, lease_id: res.data.lease_id, state: res.data.state ?? null,
4200
+ reap_reason: res.data.reap_reason ?? null, ...(res.data?.message ? { message: res.data.message } : {}) } };
4201
+ }
4102
4202
  if (ADMISSION_REPLAYABLE.has(code)) replayCode = code;
4103
4203
  // After an answer that may have committed, a later refusal is not proof of absence.
4104
4204
  else if (mayHaveCommitted) { settle("ADMISSION_UNCERTAIN"); return res; }
4105
- // A definite, effect-free refusal never carries cleanup authority.
4205
+ // A definite refusal never carries cleanup authority. BOT-2788: an idle lease the control
4206
+ // plane is reprovisioning (or lost to preemption) keeps its id ONLY so the run can wait
4207
+ // for its reap and send this same request again; it is never adopted or released.
4106
4208
  else return { ...res, data: { success: false, code, ...(res.data?.message ? { message: res.data.message } : {}),
4107
- ...(res.data?.phase ? { phase: res.data.phase } : {}), ...(res.data?.wait ? { wait: res.data.wait } : {}) } };
4209
+ ...(res.data?.phase ? { phase: res.data.phase } : {}), ...(res.data?.wait ? { wait: res.data.wait } : {}),
4210
+ ...(code === HOST_CAPACITY_SOURCE_STALE_CODE ? capacityStaleReceiptFields(res.data) : {}) } };
4108
4211
  } else {
4109
4212
  const ack = identifiedAdmissionAck(res.data, leaseArgs.request_id);
4110
4213
  // A malformed or foreign ACK is untrusted: never cleanup authority, never adopted.
4111
- if (!ack || ack.admission_kind !== "created" || (ownedAck && ack.lease_id !== ownedAck.lease_id)) {
4214
+ if (!ack || (ownedAck && ack.lease_id !== ownedAck.lease_id)) {
4112
4215
  mayHaveCommitted = true;
4113
4216
  if (ack && ownedAck && ack.lease_id !== ownedAck.lease_id) ownedAck = null; // two leases for one key: trust neither
4114
- settle(!ack || ack.admission_kind === "created" ? "ADMISSION_UNCERTAIN" : "ADMISSION_READOPTION_UNSUPPORTED");
4217
+ settle("ADMISSION_UNCERTAIN");
4115
4218
  return res;
4116
4219
  }
4117
4220
  const delivery = identifiedAdmissionDelivery(res.data, ack);
4221
+ // BOT-2788: a readopted ACK is this slot's own lease handed back: an idle stack (stored
4222
+ // connection, nothing provisioned) takes the idle-reuse path; a queued place kept at a
4223
+ // park deadline takes the BOT-2400 re-attach path (no second upload).
4224
+ if (delivery && ack.admission_kind === "readopted") {
4225
+ return { ...res, data: { ...delivery, reused: true, ...(ack.state === "queued" ? {} : { reused_idle: true }) } };
4226
+ }
4118
4227
  if (delivery) return { ...res, data: delivery };
4119
4228
  if (ack.delivery_status === "ready") { mayHaveCommitted = true; settle("ADMISSION_UNCERTAIN"); return res; }
4120
4229
  ownedAck = ack;
@@ -4226,8 +4335,12 @@ async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
4226
4335
  // BOT-2400 (item 6): this agent's OWN previous lease on the slot is still `reaping` (a
4227
4336
  // provision rollback, an idle reap, the previous batch's teardown). It is not somebody's
4228
4337
  // live batch: park until it is reaped, then send the same request again.
4229
- const ownReaping = request.data?.success === true && request.data.reused === true &&
4230
- request.data.state === "reaping" && request.data.lease_id ? request.data.lease_id : null;
4338
+ const ownReaping = (request.data?.success === true && request.data.reused === true &&
4339
+ request.data.state === "reaping" && request.data.lease_id) ||
4340
+ // BOT-2788: the identified admission's typed form of the same answer.
4341
+ (request.data?.success === false && request.data.code === "SLOT_REAPING" && request.data.lease_id)
4342
+ ? request.data.lease_id : null;
4343
+ if (ownReaping) idleLease.slotReaping = ownReaping;
4231
4344
  if (ownReaping && attempt < 3) {
4232
4345
  stderrLine(`stack: this slot's previous lease ${ownReaping} is still reaping — waiting for it to reach reaped, then requesting again`);
4233
4346
  activeWaitAbort = new AbortController();
@@ -4282,6 +4395,13 @@ async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
4282
4395
  };
4283
4396
  }
4284
4397
  why = READOPT_UNHEALTHY_REASON;
4398
+ if (renewIdentifiedRequest) {
4399
+ try { renewIdentifiedRequest(); } catch (error) {
4400
+ retainBlock = true;
4401
+ return { exitCode: EXIT.INTERNAL, outcome: "error", code: "REQUESTER_ORIGIN_CONTEXT_REFUSED", leaseId: broken,
4402
+ error: `could not record a new admission request after reaping unhealthy idle lease ${broken} (${error?.message ?? error}); no new lease was requested` };
4403
+ }
4404
+ }
4285
4405
  }
4286
4406
  }
4287
4407
  if (!why) break;
@@ -4364,6 +4484,9 @@ async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
4364
4484
 
4365
4485
  leaseProvisioned = true; // from here a Helper may act on this lease
4366
4486
  execution.bindLease?.(leaseId);
4487
+ // BOT-2788: the lease's stack (or its kept place) now names this origin; record it for the
4488
+ // next run only once admitted, so a refusal never moves an idle stack's recorded origin.
4489
+ if (execution.requesterOrigin) { try { execution.recordOrigin?.(); } catch { /* advisory: the next run reprovisions */ } }
4367
4490
  let state = request.data.state;
4368
4491
  if (reusedIdle) {
4369
4492
  idleLease.reused = "idle";
@@ -4510,7 +4633,9 @@ async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
4510
4633
  // `stack cancel` / `stack done` or the owner's death releases it.
4511
4634
  // BOT-2780: a requester-origin run cannot be resumed by a later invocation (its stack
4512
4635
  // names this CLI's listener, which closes now), so it releases instead of holding a place.
4513
- const queueHold = holdPlace && !execution.requesterOrigin ? await holdPlace(leaseId, HOLD_ON_TIMEOUT_MAX_SEC) : null;
4636
+ // BOT-2788: a stable origin run keeps its place too — its marker keeps the origin, so the
4637
+ // next run re-reserves it and re-attaches (the same bundle variant) through identified admission.
4638
+ const queueHold = holdPlace && !(execution.requesterOrigin && opts.executionRing === "beta") ? await holdPlace(leaseId, HOLD_ON_TIMEOUT_MAX_SEC) : null;
4514
4639
  if (queueHold) {
4515
4640
  // A refused/lost hold reply is not an owner cancellation: it may have
4516
4641
  // committed before the reply was lost, or the queued lease may have
@@ -4704,6 +4829,7 @@ async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
4704
4829
  return { exitCode: cleanup.ok ? EXIT.BACKEND : EXIT.CLEANUP_FAILED, outcome: "error", code: error?.code ?? "REQUESTER_ORIGIN_FORWARD_REFUSED", leaseId, error: String(error?.message ?? error), cleanup };
4705
4830
  }
4706
4831
  placement.requesterOrigin = execution.requesterOrigin.origin;
4832
+ placement.requesterOriginContinuation = execution.originContinuation ?? null;
4707
4833
  stderrLine(`stack: requester origin ${execution.requesterOrigin.origin} → ${placement.hostKey ?? "remote"} API (generated auth links use it unchanged)`);
4708
4834
  childConnection = { ...childConnection, requester_origin: execution.requesterOrigin.origin };
4709
4835
  }
@@ -4860,6 +4986,14 @@ async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
4860
4986
  // once, or repeated refusals would exhaust the host (Codex #928 R21 P2).
4861
4987
  // BOT-1803: an IDLE lease (cleanupResult.state "active") is not a reap — its stack still
4862
4988
  // runs, so its block stays reserved; so does one this run must not free (retainBlock).
4989
+ // BOT-2788: an origin run that resumed an idle stack's directory but never took a lease of
4990
+ // its own (a refusal) leaves that idle stack exactly as it found it: its block stays
4991
+ // reserved and its marker idle-retained, so a later run can still continue it.
4992
+ if (execution?.resumedIdle && !leaseId && !idleLease.previousLeaseId && !idleLease.slotReaping) {
4993
+ retainBlock = true;
4994
+ try { refreshRetention(stackDirFor(execution), { idleTtlSecs: LEASED_STACK_IDLE_RETENTION_SECS, idleRetained: true }); } catch { /* advisory */ }
4995
+ try { execution.renew?.({ idleTtlSecs: LEASED_STACK_IDLE_RETENTION_SECS, idleRetained: true }); } catch { /* advisory */ }
4996
+ }
4863
4997
  const provenReap = !retainBlock && (!leaseProvisioned
4864
4998
  || (cleanupResult?.ok === true && (cleanupResult.state === "reaped" || cleanupResult.state == null)));
4865
4999
  if (execution?.release) {
@@ -4920,6 +5054,8 @@ export function runReceiptFields(result) {
4920
5054
  transport: result.execution?.transport ?? "none",
4921
5055
  // BOT-2780: the reserved requester origin generated auth links name (never a credential).
4922
5056
  ...(result.execution?.requesterOrigin ? { requester_origin: result.execution.requesterOrigin } : {}),
5057
+ // BOT-2788: "previous" (an idle stack's origin re-reserved), "previous_port_busy" or "new".
5058
+ ...(result.execution?.requesterOriginContinuation ? { requester_origin_continuation: result.execution.requesterOriginContinuation } : {}),
4923
5059
  ...(result.allocationRecovery ? { allocation_recovery: result.allocationRecovery } : {}),
4924
5060
  ...(result.execution?.forwards ? { forwards: result.execution.forwards } : {}),
4925
5061
  ...(result.execution?.bundle ? { bundle: result.execution.bundle } : {}),