@botbuddy/cli 1.63.18 → 1.63.21
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/daemon/bin/darwin-arm64/botbuddy-daemon +0 -0
- package/daemon/bin/linux-x64/botbuddy-daemon +0 -0
- package/daemon-release/darwin-arm64.json +4 -4
- package/daemon-release/linux-x64.json +4 -4
- package/daemon-release/manifest.json +5 -5
- package/package.json +1 -1
- package/src/stack-isolation.mjs +43 -4
- package/src/stack-requester-origin-config.mjs +1 -1
- package/src/stack.mjs +173 -37
|
Binary file
|
|
Binary file
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"release_version": "1.63.
|
|
3
|
-
"source_commit": "
|
|
4
|
-
"source_sha256": "
|
|
2
|
+
"release_version": "1.63.21",
|
|
3
|
+
"source_commit": "ab207b617fc4964bbb38137736885e9962e71afc",
|
|
4
|
+
"source_sha256": "93fe3db3d6b3c94deb36c76aa04f51cea071e3f839082545af484e9d2abacee4",
|
|
5
5
|
"target": "darwin-arm64",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "84dd23363db44e16419fe733119d819566d8727441f309a318e342cffec5c18b"
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"release_version": "1.63.
|
|
3
|
-
"source_commit": "
|
|
4
|
-
"source_sha256": "
|
|
2
|
+
"release_version": "1.63.21",
|
|
3
|
+
"source_commit": "ab207b617fc4964bbb38137736885e9962e71afc",
|
|
4
|
+
"source_sha256": "93fe3db3d6b3c94deb36c76aa04f51cea071e3f839082545af484e9d2abacee4",
|
|
5
5
|
"target": "linux-x64",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "a8f549e6f04d9e61b8e9ff6f37dca0c4fe008d37fdd0afd35a771b2c9c0e36c5"
|
|
7
7
|
}
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": 1,
|
|
3
|
-
"release_version": "1.63.
|
|
4
|
-
"source_commit": "
|
|
5
|
-
"source_sha256": "
|
|
3
|
+
"release_version": "1.63.21",
|
|
4
|
+
"source_commit": "ab207b617fc4964bbb38137736885e9962e71afc",
|
|
5
|
+
"source_sha256": "93fe3db3d6b3c94deb36c76aa04f51cea071e3f839082545af484e9d2abacee4",
|
|
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": "
|
|
11
|
+
"sha256": "84dd23363db44e16419fe733119d819566d8727441f309a318e342cffec5c18b",
|
|
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": "
|
|
18
|
+
"sha256": "a8f549e6f04d9e61b8e9ff6f37dca0c4fe008d37fdd0afd35a771b2c9c0e36c5",
|
|
19
19
|
"mode": "0755"
|
|
20
20
|
}
|
|
21
21
|
}
|
package/package.json
CHANGED
package/src/stack-isolation.mjs
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
66
|
-
* origin and allocates through BOT-2499's identified admission.
|
|
67
|
-
*
|
|
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) =>
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
|
913
|
-
//
|
|
914
|
-
//
|
|
915
|
-
|
|
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
|
-
//
|
|
994
|
-
//
|
|
995
|
-
|
|
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
|
-
|
|
998
|
-
//
|
|
999
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 ||
|
|
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(
|
|
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
|
|
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
|
-
|
|
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 } : {}),
|