@botbuddy/cli 1.29.0 → 1.29.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/stack.mjs +43 -19
package/package.json
CHANGED
package/src/stack.mjs
CHANGED
|
@@ -30,6 +30,7 @@ import { randomUUID } from "crypto";
|
|
|
30
30
|
import { callToolJson } from "./api.mjs";
|
|
31
31
|
import { SERVER_URL, getConfig } from "./config.mjs";
|
|
32
32
|
import { resolveOwnerToken, resolveAgentKey } from "./cli-credentials.mjs";
|
|
33
|
+
import { AGENT_KEY_RE, readAgentKeyEnv } from "./agent-key.mjs";
|
|
33
34
|
import { runDockerCommand, runDockerWorkflow } from "./docker-hygiene.mjs";
|
|
34
35
|
import { machineUuid } from "./machine-id.mjs";
|
|
35
36
|
import { bold, dim, yellow } from "./utils.mjs";
|
|
@@ -352,10 +353,17 @@ export function parseSupabaseStatus(text) {
|
|
|
352
353
|
|
|
353
354
|
// ── runtime (network / process) ──────────────────────────────────────────────
|
|
354
355
|
|
|
355
|
-
// BOT-
|
|
356
|
-
//
|
|
357
|
-
//
|
|
356
|
+
// BOT-1599: a stack lease is a host-bound operation. When a session token is
|
|
357
|
+
// present it must win over the durable OAuth client credential, otherwise the
|
|
358
|
+
// lease RPC resolves the hostless/stale OAuth agent rather than the agent that
|
|
359
|
+
// registered this worktree and machine attestation. The MCP server accepts the
|
|
360
|
+
// session token through the same agent-key header used by bb-pw.
|
|
361
|
+
//
|
|
362
|
+
// Without a session token, retain the BOT-1520 Keychain-backed OAuth/MCP-key
|
|
363
|
+
// fallback for operator commands and legacy callers.
|
|
358
364
|
export async function stackAuthHeader() {
|
|
365
|
+
const sessionToken = readAgentKeyEnv();
|
|
366
|
+
if (sessionToken) return AGENT_KEY_RE.test(sessionToken) ? { "x-agent-api-key": sessionToken } : null;
|
|
359
367
|
const agentKey = await resolveAgentKey();
|
|
360
368
|
const owner = await resolveOwnerToken({ getConfig });
|
|
361
369
|
if (owner) {
|
|
@@ -697,6 +705,7 @@ export async function cmdUp(opts, {
|
|
|
697
705
|
}
|
|
698
706
|
const auth = await authProvider();
|
|
699
707
|
if (!auth) return emitResult(buildReceipt({ command: "up", outcome: "error", error: "not authenticated — run `botbuddy login`" }), opts, EXIT.AUTH);
|
|
708
|
+
const call = (name, args, callOptions = {}) => callTool(name, args, { ...callOptions, auth });
|
|
700
709
|
|
|
701
710
|
// BOT-1585: co-location dispatches the lease to the Helper enrolled for THIS
|
|
702
711
|
// physical machine (hardware id), since a hostname is not machine-unique. The
|
|
@@ -705,7 +714,7 @@ export async function cmdUp(opts, {
|
|
|
705
714
|
if (!hardwareUuid) {
|
|
706
715
|
return emitResult(buildReceipt({ command: "up", outcome: "error", code: "MACHINE_UUID_REQUIRED", error: "could not determine this machine's hardware id (needed to dispatch the stack lease to the right machine)" }), opts, EXIT.BACKEND);
|
|
707
716
|
}
|
|
708
|
-
const req = await
|
|
717
|
+
const req = await call("request_stack_lease", {
|
|
709
718
|
slot, host_key: opts.host || undefined, repo: opts.repo || undefined,
|
|
710
719
|
ticket_id: opts.ticket || undefined, ticket_url: opts.ticketUrl || undefined,
|
|
711
720
|
pr_id: opts.prId || undefined, pr_url: opts.prUrl || undefined,
|
|
@@ -748,7 +757,7 @@ export async function cmdUp(opts, {
|
|
|
748
757
|
localPreflight = compactPreflight(checked.receipt);
|
|
749
758
|
localDockerTarget = dockerTargetFromPreflight(checked.receipt);
|
|
750
759
|
if (checked.exitCode !== 0 || !localDockerTarget) {
|
|
751
|
-
const cancelled = await
|
|
760
|
+
const cancelled = await call("cancel_unclaimed_stack_lease", { lease_id: leaseId });
|
|
752
761
|
const leaseCancellation = cancelled.ok && cancelled.data?.success
|
|
753
762
|
? { success: true, state: cancelled.data.state, provision_job_cancelled: cancelled.data.provision_job_cancelled === true }
|
|
754
763
|
: { success: false, error: cancelled.error || cancelled.data?.code || "atomic cancellation failed" };
|
|
@@ -768,7 +777,7 @@ export async function cmdUp(opts, {
|
|
|
768
777
|
// Persist the validated Docker target with the reservation so that if the
|
|
769
778
|
// provisioner partially starts a stack and then fails, the fenced lease can
|
|
770
779
|
// still be torn down by `stack done --local-exec` (BOT-1421 review).
|
|
771
|
-
const reserved = await
|
|
780
|
+
const reserved = await call("reserve_stack_lease", {
|
|
772
781
|
lease_id: leaseId,
|
|
773
782
|
connection: connectionWithDockerTarget({}, localDockerTarget),
|
|
774
783
|
});
|
|
@@ -801,7 +810,7 @@ export async function cmdUp(opts, {
|
|
|
801
810
|
// Fall through to the authoritative `get_stack_lease` read below — do
|
|
802
811
|
// NOT run the local provisioner; the Helper owns this stack.
|
|
803
812
|
} else {
|
|
804
|
-
const cancelled = await
|
|
813
|
+
const cancelled = await call("cancel_unclaimed_stack_lease", { lease_id: leaseId });
|
|
805
814
|
const leaseCancellation = cancelled.ok && cancelled.data?.success
|
|
806
815
|
? { success: true, state: cancelled.data.state, provision_job_cancelled: cancelled.data.provision_job_cancelled === true }
|
|
807
816
|
: { success: false, error: cancelled.error || cancelled.data?.code || "atomic cancellation failed" };
|
|
@@ -825,7 +834,7 @@ export async function cmdUp(opts, {
|
|
|
825
834
|
// review); do NOT cancel.
|
|
826
835
|
return emitResult(buildReceipt({ command: "up", outcome: "error", lease_id: leaseId, error: e.message }), opts, EXIT.LEASE_FAILED);
|
|
827
836
|
}
|
|
828
|
-
const act = await
|
|
837
|
+
const act = await call("activate_stack_lease", { lease_id: leaseId, connection: conn });
|
|
829
838
|
if (!act.ok || !act.data?.success) {
|
|
830
839
|
return emitResult(buildReceipt({ command: "up", outcome: "error", lease_id: leaseId, error: act.error || act.data?.code || "activate failed" }), opts, EXIT.BACKEND);
|
|
831
840
|
}
|
|
@@ -840,7 +849,7 @@ export async function cmdUp(opts, {
|
|
|
840
849
|
}
|
|
841
850
|
|
|
842
851
|
// Authoritative final read (connection block, current state).
|
|
843
|
-
const got = await
|
|
852
|
+
const got = await call("get_stack_lease", { lease_id: leaseId });
|
|
844
853
|
const g = got.ok && got.data?.success ? got.data : null;
|
|
845
854
|
if (!g || g.state !== "active") {
|
|
846
855
|
return emitResult(buildReceipt({ command: "up", outcome: "error", lease_id: leaseId, state: g?.state, error: g ? `lease is ${g.state}, not active` : (got.error || "could not read lease") }), opts, g?.state && isReaped(g.state) ? EXIT.LEASE_FAILED : EXIT.BACKEND);
|
|
@@ -854,7 +863,9 @@ export async function cmdUp(opts, {
|
|
|
854
863
|
}
|
|
855
864
|
|
|
856
865
|
async function cmdStatus(leaseId, opts) {
|
|
857
|
-
const
|
|
866
|
+
const auth = await stackAuthHeader();
|
|
867
|
+
if (!auth) return emit(buildReceipt({ command: "status", outcome: "error", lease_id: leaseId, error: "not authenticated — run `botbuddy login`" }), opts, EXIT.AUTH);
|
|
868
|
+
const got = await callToolJson("get_stack_lease", { lease_id: leaseId }, { auth });
|
|
858
869
|
if (!got.ok) return got.auth
|
|
859
870
|
? emit(buildReceipt({ command: "status", outcome: "error", lease_id: leaseId, error: got.error }), opts, EXIT.AUTH)
|
|
860
871
|
: emit(buildReceipt({ command: "status", outcome: "error", lease_id: leaseId, error: got.error }), opts, EXIT.BACKEND);
|
|
@@ -868,7 +879,9 @@ async function cmdStatus(leaseId, opts) {
|
|
|
868
879
|
}
|
|
869
880
|
|
|
870
881
|
async function cmdTouch(leaseId, opts) {
|
|
871
|
-
const
|
|
882
|
+
const auth = await stackAuthHeader();
|
|
883
|
+
if (!auth) return emit(buildReceipt({ command: "touch", outcome: "error", lease_id: leaseId, error: "not authenticated — run `botbuddy login`" }), opts, EXIT.AUTH);
|
|
884
|
+
const r = await callToolJson("touch_stack_lease", { lease_id: leaseId }, { auth });
|
|
872
885
|
if (!r.ok) return emit(buildReceipt({ command: "touch", outcome: "error", lease_id: leaseId, error: r.error }), opts, r.auth ? EXIT.AUTH : EXIT.BACKEND);
|
|
873
886
|
if (!r.data.success) return emit(buildReceipt({ command: "touch", outcome: "error", lease_id: leaseId, code: r.data.code, error: r.data.code }), opts, EXIT.BACKEND);
|
|
874
887
|
return emit(buildReceipt({ command: "touch", outcome: "touched", lease_id: leaseId, last_used_at: r.data.last_used_at }), opts, EXIT.OK);
|
|
@@ -876,15 +889,19 @@ async function cmdTouch(leaseId, opts) {
|
|
|
876
889
|
|
|
877
890
|
export async function cmdDone(leaseId, opts, {
|
|
878
891
|
callTool = callToolJson,
|
|
892
|
+
authProvider = stackAuthHeader,
|
|
879
893
|
runPreflight = runLocalExecTargetCheck,
|
|
880
894
|
proveLegacyTarget = proveLegacyLocalExecTarget,
|
|
881
895
|
localTeardownFn = localTeardown,
|
|
882
896
|
emitResult = emit,
|
|
883
897
|
} = {}) {
|
|
898
|
+
const auth = await authProvider();
|
|
899
|
+
if (!auth) return emitResult(buildReceipt({ command: "done", outcome: "error", lease_id: leaseId, error: "not authenticated — run `botbuddy login`" }), opts, EXIT.AUTH);
|
|
900
|
+
const call = (name, args, callOptions = {}) => callTool(name, args, { ...callOptions, auth });
|
|
884
901
|
let dockerTarget = null;
|
|
885
902
|
let legacyTargetProof = null;
|
|
886
903
|
if (opts.localExec) {
|
|
887
|
-
const current = await
|
|
904
|
+
const current = await call("get_stack_lease", { lease_id: leaseId });
|
|
888
905
|
if (!current.ok || !current.data?.success) {
|
|
889
906
|
return emitResult(buildReceipt({ command: "done", outcome: "error", lease_id: leaseId,
|
|
890
907
|
error: current.error || current.data?.code || "could not verify the lease Docker target" }), opts,
|
|
@@ -915,7 +932,7 @@ export async function cmdDone(leaseId, opts, {
|
|
|
915
932
|
}
|
|
916
933
|
}
|
|
917
934
|
|
|
918
|
-
const r = await
|
|
935
|
+
const r = await call("release_stack_lease", { lease_id: leaseId, disposition: opts.disposition });
|
|
919
936
|
if (!r.ok) return emitResult(buildReceipt({ command: "done", outcome: "error", lease_id: leaseId, error: r.error }), opts, r.auth ? EXIT.AUTH : EXIT.BACKEND);
|
|
920
937
|
if (!r.data.success) return emitResult(buildReceipt({ command: "done", outcome: "error", lease_id: leaseId, code: r.data.code, error: r.data.code }), opts, EXIT.BACKEND);
|
|
921
938
|
let state = r.data.state;
|
|
@@ -931,7 +948,7 @@ export async function cmdDone(leaseId, opts, {
|
|
|
931
948
|
error: "local `supabase stop` failed — NOT finalizing; the slot stays fenced. Tear the stack down and re-run `stack done --local-exec`, or let the reaper reconcile.",
|
|
932
949
|
}), opts, EXIT.LEASE_FAILED);
|
|
933
950
|
}
|
|
934
|
-
const fin = await
|
|
951
|
+
const fin = await call("finalize_stack_lease", { lease_id: leaseId });
|
|
935
952
|
if (fin.ok && fin.data?.success) state = fin.data.state;
|
|
936
953
|
else process.stderr.write(`${yellow("⚠")} stack: finalize failed (${fin.error || fin.data?.code}); the reaper will reconcile.\n`);
|
|
937
954
|
}
|
|
@@ -1049,13 +1066,13 @@ export async function materializeLeasedTestConfig(worktreeRoot, leaseId, connect
|
|
|
1049
1066
|
* without Docker or a live BotBuddy service.
|
|
1050
1067
|
*/
|
|
1051
1068
|
export async function runStackLifecycle(opts, childArgv, adapters = {}) {
|
|
1069
|
+
const auth = adapters.auth ?? await stackAuthHeader();
|
|
1052
1070
|
const api = adapters.api ?? {
|
|
1053
|
-
request: (args) => callToolJson("request_stack_lease", args),
|
|
1054
|
-
get: (leaseId) => callToolJson("get_stack_lease", { lease_id: leaseId }),
|
|
1055
|
-
touch: (leaseId) => callToolJson("touch_stack_lease", { lease_id: leaseId }),
|
|
1056
|
-
release: (leaseId, signal) => callToolJson("release_stack_lease", { lease_id: leaseId, disposition: "destroy" }, { signal }),
|
|
1071
|
+
request: (args) => callToolJson("request_stack_lease", args, { auth }),
|
|
1072
|
+
get: (leaseId) => callToolJson("get_stack_lease", { lease_id: leaseId }, { auth }),
|
|
1073
|
+
touch: (leaseId) => callToolJson("touch_stack_lease", { lease_id: leaseId }, { auth }),
|
|
1074
|
+
release: (leaseId, signal) => callToolJson("release_stack_lease", { lease_id: leaseId, disposition: "destroy" }, { auth, signal }),
|
|
1057
1075
|
};
|
|
1058
|
-
const auth = adapters.auth ?? await stackAuthHeader();
|
|
1059
1076
|
const machineUuidFn = adapters.machineUuidFn ?? machineUuid;
|
|
1060
1077
|
const wait = adapters.wait ?? ((leaseId, done, failed, options) => waitForLease(leaseId, done, failed, options));
|
|
1061
1078
|
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process -- validated executable + argv only; shell is never used.
|
|
@@ -1263,6 +1280,13 @@ export async function cmdStack(argv) {
|
|
|
1263
1280
|
process.exitCode = code;
|
|
1264
1281
|
return code;
|
|
1265
1282
|
}
|
|
1283
|
+
const sessionToken = readAgentKeyEnv();
|
|
1284
|
+
if (sessionToken && !AGENT_KEY_RE.test(sessionToken)) {
|
|
1285
|
+
process.stderr.write(`${yellow("⚠")} stack: $BOTBUDDY_AGENT_KEY must match bb_agent_<64 hex>; re-register and export a fresh session token.\n`);
|
|
1286
|
+
const code = emit(buildReceipt({ command, outcome: "error", error: "invalid_session_token" }), opts, EXIT.INVALID);
|
|
1287
|
+
process.exitCode = code;
|
|
1288
|
+
return code;
|
|
1289
|
+
}
|
|
1266
1290
|
let code;
|
|
1267
1291
|
try {
|
|
1268
1292
|
switch (command) {
|