@botbuddy/cli 1.60.9 → 1.61.0
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 +2 -4
- package/src/commands.mjs +3 -2
- package/src/evidence.mjs +38 -14
- package/src/pw/run.mjs +9 -1
- package/src/staging-pool/errors.mjs +8 -1
- package/src/staging-pool/fixtures/2026-10-05-botbuddy-get-totp-code.json +11 -0
- package/src/staging-pool/fixtures/2026-10-05-gotrue-session-lifecycle.json +37 -0
- package/src/staging-pool/test-helpers/bindings.mjs +41 -0
- package/src/staging-pool/test-helpers/fake-signin.mjs +60 -0
- package/src/staging-pool/test-helpers/take-worker.mjs +28 -0
|
Binary file
|
|
Binary file
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"release_version": "1.
|
|
3
|
-
"source_commit": "
|
|
4
|
-
"source_sha256": "
|
|
2
|
+
"release_version": "1.61.0",
|
|
3
|
+
"source_commit": "5661e7d4d29c975fc925b0a6c47741db33598642",
|
|
4
|
+
"source_sha256": "c5c57ed492234165bee179842456b3bebce505b36145e96551c0ddbd0cdf605a",
|
|
5
5
|
"target": "darwin-arm64",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "afadb8e224f68209f4a2a0f8367d4aa289a76ac21f2ee57cbd376b90ab569469"
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"release_version": "1.
|
|
3
|
-
"source_commit": "
|
|
4
|
-
"source_sha256": "
|
|
2
|
+
"release_version": "1.61.0",
|
|
3
|
+
"source_commit": "5661e7d4d29c975fc925b0a6c47741db33598642",
|
|
4
|
+
"source_sha256": "c5c57ed492234165bee179842456b3bebce505b36145e96551c0ddbd0cdf605a",
|
|
5
5
|
"target": "linux-x64",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "830ff851e5d9d85957855af52a7c372cde56e2f9ddb315a31f5d2477c1c7d9d7"
|
|
7
7
|
}
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": 1,
|
|
3
|
-
"release_version": "1.
|
|
4
|
-
"source_commit": "
|
|
5
|
-
"source_sha256": "
|
|
3
|
+
"release_version": "1.61.0",
|
|
4
|
+
"source_commit": "5661e7d4d29c975fc925b0a6c47741db33598642",
|
|
5
|
+
"source_sha256": "c5c57ed492234165bee179842456b3bebce505b36145e96551c0ddbd0cdf605a",
|
|
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": "afadb8e224f68209f4a2a0f8367d4aa289a76ac21f2ee57cbd376b90ab569469",
|
|
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": "830ff851e5d9d85957855af52a7c372cde56e2f9ddb315a31f5d2477c1c7d9d7",
|
|
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.
|
|
3
|
+
"version": "1.61.0",
|
|
4
4
|
"description": "BotBuddy \u2014 Swarm coordination CLI for multi-agent workflows",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -19,9 +19,7 @@
|
|
|
19
19
|
"daemon/",
|
|
20
20
|
"daemon-release/",
|
|
21
21
|
"src/",
|
|
22
|
-
"!src/**/*.test.mjs"
|
|
23
|
-
"!src/staging-pool/test-helpers/",
|
|
24
|
-
"!src/staging-pool/fixtures/"
|
|
22
|
+
"!src/**/*.test.mjs"
|
|
25
23
|
],
|
|
26
24
|
"keywords": [
|
|
27
25
|
"mcp",
|
package/src/commands.mjs
CHANGED
|
@@ -278,12 +278,13 @@ ${bold("STAGING SIGN-IN")} (BOT-2084)
|
|
|
278
278
|
persona, no password (${cyan("bb pw auth --help")})
|
|
279
279
|
staging-pool status [--check]
|
|
280
280
|
Pool health: one reason, who acts, the next command
|
|
281
|
-
staging-pool install|tick|mint-one|uninstall
|
|
282
|
-
Human only,
|
|
281
|
+
staging-pool install --tenant <slug> | tick | mint-one | uninstall
|
|
282
|
+
Human only, any directory (${cyan("bb staging-pool --help")})
|
|
283
283
|
help symptom staging-pool Every refusal code with its cause and next command
|
|
284
284
|
|
|
285
285
|
${bold("OTHER")}
|
|
286
286
|
locks -m [--host name] Reserve typed local resources, including Playwright MCP lanes
|
|
287
|
+
evidence require <file> Record an exact-head evidence disposition → botbuddy/ready
|
|
287
288
|
evidence publish <file> Publish a local evidence statement → botbuddy/ready check-run
|
|
288
289
|
update Install the latest @botbuddy/cli in the background via npm
|
|
289
290
|
help Show this help
|
package/src/evidence.mjs
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// BOT-2341: `bb evidence publish <statement.json>` — post a local-E2E evidence
|
|
2
2
|
// statement to the control plane (MCP `publish_evidence`), which stores it and
|
|
3
|
-
// creates/updates the
|
|
3
|
+
// creates/updates the `botbuddy/ready` check-run on the PR head.
|
|
4
|
+
// BOT-786: `bb evidence require <requirement.json>` — record the impact
|
|
5
|
+
// producer's exact-head disposition (MCP `publish_evidence_requirement`):
|
|
6
|
+
// not_required completes botbuddy/ready as success on that head, e2e queues it.
|
|
4
7
|
//
|
|
5
8
|
// Auth prefers this worktree's agent session (the tenant the repo is bound to),
|
|
6
9
|
// falling back to the owner/MCP-key ladder like every other `bb` call. Prints
|
|
@@ -11,14 +14,25 @@ import { CALL_EXIT, callToolJson as defaultCallToolJson, resolveCallAuth } from
|
|
|
11
14
|
|
|
12
15
|
export const EVIDENCE_EXIT = Object.freeze({ ...CALL_EXIT, USAGE: 2, REFUSED: 4 });
|
|
13
16
|
|
|
14
|
-
const USAGE = `Usage: bb evidence
|
|
17
|
+
const USAGE = `Usage: bb evidence require <requirement.json>
|
|
18
|
+
bb evidence publish <statement.json>
|
|
15
19
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
require Record the impact producer's decision for one exact PR head. The file
|
|
21
|
+
is a JSON object with: repo, pr, headSha, disposition
|
|
22
|
+
("not_required" | "e2e"), reason, command (required for e2e).
|
|
23
|
+
not_required completes botbuddy/ready as success on that head; e2e
|
|
24
|
+
queues it with the command to run.
|
|
25
|
+
publish Publish a local evidence statement for a PR head (after an e2e
|
|
26
|
+
requirement). The statement is a JSON object with:
|
|
27
|
+
repo, pr, headSha, mainSha, base, fingerprint, requirement, runs[]
|
|
28
|
+
(each with the BotBuddy runId), verdict ("pass" | "fail"), fixCommand
|
|
20
29
|
|
|
21
|
-
Exit codes: 0
|
|
30
|
+
Exit codes: 0 recorded · 2 usage · 3 auth · 4 refused · 5 unreachable`;
|
|
31
|
+
|
|
32
|
+
const SUBCOMMANDS = Object.freeze({
|
|
33
|
+
require: { tool: "publish_evidence_requirement", argName: "requirement" },
|
|
34
|
+
publish: { tool: "publish_evidence", argName: "statement" },
|
|
35
|
+
});
|
|
22
36
|
|
|
23
37
|
export async function cmdEvidence(args, {
|
|
24
38
|
resolveAuth = resolveCallAuth,
|
|
@@ -31,7 +45,8 @@ export async function cmdEvidence(args, {
|
|
|
31
45
|
log(USAGE);
|
|
32
46
|
return sub === undefined ? EVIDENCE_EXIT.USAGE : EVIDENCE_EXIT.OK;
|
|
33
47
|
}
|
|
34
|
-
|
|
48
|
+
const spec = Object.hasOwn(SUBCOMMANDS, sub) ? SUBCOMMANDS[sub] : null;
|
|
49
|
+
if (!spec) {
|
|
35
50
|
errorLog(`Unknown evidence subcommand: ${sub}\n\n${USAGE}`);
|
|
36
51
|
return EVIDENCE_EXIT.USAGE;
|
|
37
52
|
}
|
|
@@ -40,22 +55,22 @@ export async function cmdEvidence(args, {
|
|
|
40
55
|
return EVIDENCE_EXIT.USAGE;
|
|
41
56
|
}
|
|
42
57
|
|
|
43
|
-
let
|
|
58
|
+
let body;
|
|
44
59
|
try {
|
|
45
|
-
|
|
60
|
+
body = JSON.parse(readFileSync(file, "utf8"));
|
|
46
61
|
} catch (error) {
|
|
47
62
|
errorLog(`✗ Could not read ${file} as JSON: ${error?.message ?? error}`);
|
|
48
63
|
return EVIDENCE_EXIT.USAGE;
|
|
49
64
|
}
|
|
50
65
|
|
|
51
|
-
const resolved = await resolveAuth({ toolName:
|
|
66
|
+
const resolved = await resolveAuth({ toolName: spec.tool, preferSession: true });
|
|
52
67
|
if (resolved?.error || !resolved?.auth) {
|
|
53
68
|
log(JSON.stringify(resolved?.error ?? { ok: false, auth: true, error: "not_authenticated" }, null, 2));
|
|
54
69
|
errorLog("✗ Not authenticated — run `bb doctor --fix` in this worktree.");
|
|
55
70
|
return EVIDENCE_EXIT.AUTH;
|
|
56
71
|
}
|
|
57
72
|
|
|
58
|
-
const res = await callToolJson(
|
|
73
|
+
const res = await callToolJson(spec.tool, { [spec.argName]: body }, { auth: resolved.auth });
|
|
59
74
|
if (!res.ok) {
|
|
60
75
|
log(JSON.stringify(res, null, 2));
|
|
61
76
|
if (res.auth) {
|
|
@@ -66,15 +81,24 @@ export async function cmdEvidence(args, {
|
|
|
66
81
|
errorLog(`✗ BotBuddy unreachable: ${res.error}`);
|
|
67
82
|
return EVIDENCE_EXIT.TRANSPORT;
|
|
68
83
|
}
|
|
69
|
-
errorLog(`✗
|
|
84
|
+
errorLog(`✗ ${spec.tool} failed: ${res.error}`);
|
|
70
85
|
return EVIDENCE_EXIT.ERROR;
|
|
71
86
|
}
|
|
72
87
|
|
|
73
88
|
log(JSON.stringify(res.data, null, 2));
|
|
74
89
|
if (res.isError || res.data?.ok === false) {
|
|
75
|
-
errorLog(`✗ ${res.data?.error ??
|
|
90
|
+
errorLog(`✗ ${res.data?.error ?? `${spec.tool} refused`}: ${res.data?.message ?? ""}`.trimEnd());
|
|
76
91
|
return EVIDENCE_EXIT.REFUSED;
|
|
77
92
|
}
|
|
93
|
+
if (sub === "require") {
|
|
94
|
+
// Success means the control plane echoed the recorded disposition — an
|
|
95
|
+
// outage body that normalises to an empty result is never "recorded".
|
|
96
|
+
if (res.data?.ok !== true || typeof res.data?.disposition !== "string") {
|
|
97
|
+
errorLog("✗ BotBuddy returned no disposition — nothing is known to be recorded; retry.");
|
|
98
|
+
return EVIDENCE_EXIT.ERROR;
|
|
99
|
+
}
|
|
100
|
+
return EVIDENCE_EXIT.OK;
|
|
101
|
+
}
|
|
78
102
|
// Codex #1169 R4: success means a receipt came back — an outage body that
|
|
79
103
|
// normalises to an empty result is never "published".
|
|
80
104
|
if (res.data?.ok !== true || typeof res.data?.receipt_id !== "string") {
|
package/src/pw/run.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { VERSION } from "../version.mjs";
|
|
|
11
11
|
import { readAgentSessionTokenEnv, AGENT_KEY_RE, legacyAgentAliasHint } from "../agent-key.mjs";
|
|
12
12
|
import { clearAgentState, isRejectedCachedMcpSession, resolveAgentSessionCredential } from "../agent-session.mjs";
|
|
13
13
|
import * as stagingAuth from "./staging-auth.mjs";
|
|
14
|
+
import { DOC_SECTIONS, DOC_URL } from "../staging-pool/errors.mjs";
|
|
14
15
|
// BOT-1488: canonicalize the raw hostname the SAME way acquire_resources does
|
|
15
16
|
// server-side, so the lane name bb-pw builds/matches/prints is the one the lock
|
|
16
17
|
// kernel actually stored ("jonos-mbp:8", not "Jonos-MBP.localdomain:8").
|
|
@@ -134,7 +135,12 @@ export async function runPw(argv, deps = {}) {
|
|
|
134
135
|
// while-loop in runPwInner). Everything after the first non-option (the lane) is a
|
|
135
136
|
// Playwright workload argument and is never a BotBuddy credential.
|
|
136
137
|
const PW_LEADING_FLAGS = ["--tenant", "--session-id", "--agent-session-token", "--agent-key", "--session-token", "--environment"];
|
|
137
|
-
|
|
138
|
+
// BOT-2648 acceptance: with no tenant binding, `bb doctor --fix` cannot help: the
|
|
139
|
+
// caller is outside a registered agent worktree, which lane locks need.
|
|
140
|
+
const NO_BINDING_NEXT = "Agent: run this from your registered project worktree (the checkout with .botbuddy-agent.json; run `bb setup` there once), then retry. bb help symptom tenant_binding_required";
|
|
141
|
+
const selfHealErrorLine = (error) => error?.code === "tenant_binding_required"
|
|
142
|
+
? `bb-pw: tenant_binding_required — ${error?.message ?? "no BotBuddy tenant binding here"} ${NO_BINDING_NEXT}. Docs: ${DOC_URL}#${DOC_SECTIONS.take}\n`
|
|
143
|
+
: `bb-pw: ${error?.message ?? "agent session unavailable"}. ${error?.code === "client_key_required" ? "Run `bb login` then retry." : "Run `bb doctor --fix` then retry."}\n`;
|
|
138
144
|
// The leading credential options, NORMALIZED to split `flag value` pairs so an
|
|
139
145
|
// inline `--agent-key=<token>` is recognized by legacyAgentAliasHint (which
|
|
140
146
|
// matches exact flag tokens) exactly like the split form. Credential flags are
|
|
@@ -247,6 +253,8 @@ async function runPwInner(argv, deps = {}) {
|
|
|
247
253
|
// BOT-2648 (Codex P2, #1323): the credential's tenant picks the installed staging pool.
|
|
248
254
|
deps = { ...deps, sessionToken: credential.token, sessionId: deps.sessionId ?? credential.sessionId ?? null, credentialAgentId: credential.agentId ?? null, credentialTenant: credential.identity?.tenant ?? null };
|
|
249
255
|
} catch (error) {
|
|
256
|
+
// BOT-2648 acceptance: an auth refusal is the standard JSON receipt (actor, next, doc, symptom).
|
|
257
|
+
if (plan.mode === "auth" && error?.code === "tenant_binding_required") return stagingAuth.refuse({ stdout, stderr }, "tenant_binding_required", 3, {}, authVars(plan, deps));
|
|
250
258
|
stderr.write(selfHealErrorLine(error)); return 3;
|
|
251
259
|
}
|
|
252
260
|
}
|
|
@@ -52,6 +52,13 @@ export const ERROR_CODES = Object.freeze({
|
|
|
52
52
|
"The lane lock is held by another agent session of your owner, not by this session.",
|
|
53
53
|
"The lane was claimed from a different session or sub-agent; auth needs the exact holder.",
|
|
54
54
|
`bb locks -m --lane {lane}{ticketFlag} # from THIS session, then: ${AUTH}`),
|
|
55
|
+
// BOT-2648 acceptance: the take runs from a registered agent worktree. Lane locks
|
|
56
|
+
// are per agent worktree, so a directory with no .botbuddy-agent.json has no
|
|
57
|
+
// agent identity to hold the lane (the pool itself never needs a checkout).
|
|
58
|
+
tenant_binding_required: entry("agent", "take",
|
|
59
|
+
"bb pw needs a registered agent worktree, and this directory has no BotBuddy tenant binding.",
|
|
60
|
+
"No .botbuddy-agent.json was found here or above, so there is no per-worktree agent identity to hold the lane lock.",
|
|
61
|
+
`bb setup # run inside your registered project worktree (the checkout with .botbuddy-agent.json); then from there: bb locks -m --lane {lane}{ticketFlag} && ${AUTH}`),
|
|
55
62
|
persona_not_allowlisted: entry("agent", "take",
|
|
56
63
|
"Persona \"{persona}\" is not one of this repository's staging_session_pool personas.",
|
|
57
64
|
"A typo, or an email where the alias is expected. Allowed aliases: {allowed}.",
|
|
@@ -310,7 +317,7 @@ export const ERROR_CODES = Object.freeze({
|
|
|
310
317
|
// Codes only `bb pw … auth` can raise; every other code can come from `bb staging-pool`.
|
|
311
318
|
export const AUTH_ONLY_CODES = Object.freeze(new Set([
|
|
312
319
|
"auth_requires_staging_environment", "auth_requires_lane_lock", "lane_not_claimed", "exact_lane_holder_required",
|
|
313
|
-
"persona_not_allowlisted", "lane_browser_not_open", "browser_session_not_staging", "persistent_profile_refused",
|
|
320
|
+
"tenant_binding_required", "persona_not_allowlisted", "lane_browser_not_open", "browser_session_not_staging", "persistent_profile_refused",
|
|
314
321
|
"session_install_failed", "staging_session_cleanup_pending", "session_not_aal2", "session_expired",
|
|
315
322
|
"session_persona_mismatch", "slot_state_invalid", "slot_state_unreadable", "daemon_node_unsupported",
|
|
316
323
|
"daemon_run_failed", "daemon_result_missing", "daemon_result_invalid", "pool_take_failed", "pool_empty",
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_captured": "2026-10-05 from `botbuddy call get_totp_code --json {app_slug:supplyspark-ent, seed_user_slug:seed-super-admin@supplyguard.local, environment:local}` (@botbuddy/cli 1.40.4, launchd-like env -i). Key set and value types are real; the code value is replaced and remaining_seconds is illustrative.",
|
|
3
|
+
"app_slug": "supplyspark-ent",
|
|
4
|
+
"seed_user_slug": "seed-super-admin@supplyguard.local",
|
|
5
|
+
"environment": "local",
|
|
6
|
+
"authz": "org",
|
|
7
|
+
"code": "000000",
|
|
8
|
+
"remaining_seconds": 17,
|
|
9
|
+
"digits": 6,
|
|
10
|
+
"period": 30
|
|
11
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_captured": "2026-10-05 against the ENT-5120 isolated local stack (GoTrue v2.196.0; staging reported v2.197.0 the same day). Error bodies show the error_code and msg fields as returned; tokens, which are never recorded. Experiment: scratch rotation-experiment.mjs, recorded on ENT-5120.",
|
|
3
|
+
"challengeAfterAal1SessionDeleted": {
|
|
4
|
+
"status": 403,
|
|
5
|
+
"body": {
|
|
6
|
+
"error_code": "session_not_found",
|
|
7
|
+
"msg": "Session from session_id claim in JWT does not exist"
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
"refreshAfterSessionDeleted": {
|
|
11
|
+
"status": 400,
|
|
12
|
+
"body": {
|
|
13
|
+
"error_code": "refresh_token_not_found",
|
|
14
|
+
"msg": "Invalid Refresh Token: Refresh Token Not Found"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"aal2SessionRefreshAfterAnotherMfaVerify": {
|
|
18
|
+
"status": 200,
|
|
19
|
+
"aal": "aal2"
|
|
20
|
+
},
|
|
21
|
+
"adminDeleteFactor": {
|
|
22
|
+
"status": 200,
|
|
23
|
+
"sessionsBefore": "4 sessions, all aal2",
|
|
24
|
+
"sessionsAfter": "4 sessions, all aal1 (downgraded, not deleted)"
|
|
25
|
+
},
|
|
26
|
+
"refreshAal2SessionAfterAdminDeleteFactor": {
|
|
27
|
+
"status": 200,
|
|
28
|
+
"aal": "aal1"
|
|
29
|
+
},
|
|
30
|
+
"userEndpointWithOldAal2JwtAfterAdminDeleteFactor": {
|
|
31
|
+
"status": 200
|
|
32
|
+
},
|
|
33
|
+
"reEnrolVerifyAfterDowngrade": {
|
|
34
|
+
"sessionsBefore": "4 sessions, all aal1 (downgraded)",
|
|
35
|
+
"sessionsAfter": "1 session (aal2): the downgraded aal1 sessions were deleted by the ensure script re-enrol + MFA verify"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// BOT-2084 test helper: pool bindings. SG_* mirrors supplyspark-ent's staging
|
|
2
|
+
// values (the pool's original consumer, ENT-5120) so the lifted tests keep their
|
|
3
|
+
// meaning; BOTBUDDY_* is BotBuddy's registered binding (its registration input).
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
|
|
6
|
+
import { parsePoolBinding } from "../binding.mjs";
|
|
7
|
+
import { stagingTargetFromBinding } from "../policy.mjs";
|
|
8
|
+
|
|
9
|
+
export const SG_RAW_BINDING = Object.freeze({
|
|
10
|
+
schema_version: 1,
|
|
11
|
+
tenant: "supply-guard",
|
|
12
|
+
staging_session_pool: {
|
|
13
|
+
schema_version: 1,
|
|
14
|
+
pool_id: "supplyspark-ent-staging",
|
|
15
|
+
environment: "staging",
|
|
16
|
+
app_slug: "supplyspark-ent",
|
|
17
|
+
adapter: "supabase-gotrue-totp",
|
|
18
|
+
app_origin: "https://enterprise-staging.vercel.app",
|
|
19
|
+
supabase_url: "https://ikdoydgubernmxzdqtxz.supabase.co",
|
|
20
|
+
publishable_key: "sb_publishable_gysKy2GcBNO0Gfv3d_p60Q_yMbCMUPD",
|
|
21
|
+
storage_key: "sb-ikdoydgubernmxzdqtxz-auth-token",
|
|
22
|
+
production_origins: ["https://app.supply-guard.io", "https://enterprise.vercel.app"],
|
|
23
|
+
password_env_file: "~/supply-guard/shared/.env.staging-personas",
|
|
24
|
+
personas: {
|
|
25
|
+
"super-admin": { email: "seed-super-admin@supplyguard.local", password_env: "SEED_PERSONA_PASSWORD", default_count: 2 },
|
|
26
|
+
"acme-admin": { email: "seed-customer-admin@acme-industrial.test", password_env: "SEED_PERSONA_PASSWORD", default_count: 0 },
|
|
27
|
+
"msp-admin": { email: "seed-msp-admin@supplyguard.local", password_env: "SEED_PERSONA_PASSWORD", default_count: 0 },
|
|
28
|
+
},
|
|
29
|
+
launchd_label: "com.supplyguard.staging-session-pool",
|
|
30
|
+
docs: "docs/2026-07-25-agent-workstation-notes.md#staging-session-pool",
|
|
31
|
+
install_command: "cd ~/supply-guard/development/supplyspark-ent && bb staging-pool install --botbuddy-bin \"$(command -v botbuddy)\"",
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
export const SG_BINDING = parsePoolBinding(SG_RAW_BINDING);
|
|
35
|
+
export const SG_TARGET = stagingTargetFromBinding(SG_BINDING);
|
|
36
|
+
|
|
37
|
+
// BOT-2648: BotBuddy's registration input (the staging seed job registers it in
|
|
38
|
+
// the control plane); the repo's .botbuddy-agent.json no longer carries a pool block.
|
|
39
|
+
const { _comment: _note, ...BOTBUDDY_REGISTRATION } = JSON.parse(readFileSync(new URL("../../../../scripts/staging-session-pool.botbuddy.json", import.meta.url), "utf8"));
|
|
40
|
+
export const BOTBUDDY_RAW_BINDING = Object.freeze({ schema_version: 1, ...BOTBUDDY_REGISTRATION });
|
|
41
|
+
export const BOTBUDDY_BINDING = parsePoolBinding(BOTBUDDY_RAW_BINDING);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// BOT-2084 (lifted from supplyspark-ent ENT-5120) test helper: an in-memory GoTrue stand-in for pool tests. Every mint
|
|
2
|
+
// returns a fresh aal2 session; refresh rotates the refresh token and enforces the
|
|
3
|
+
// single-holder rule (a refresh token is usable once).
|
|
4
|
+
|
|
5
|
+
let counter = 0;
|
|
6
|
+
|
|
7
|
+
export function fakeJwt(claims) {
|
|
8
|
+
const encode = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
9
|
+
return `${encode({ alg: "HS256" })}.${encode(claims)}.sig`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function fakeStoredSession({ persona, aal = "aal2", sessionId, nowMs = Date.now(), refreshToken } = {}) {
|
|
13
|
+
counter += 1;
|
|
14
|
+
const exp = Math.floor(nowMs / 1000) + 3600;
|
|
15
|
+
return {
|
|
16
|
+
access_token: fakeJwt({ aal, email: persona, exp, session_id: sessionId ?? `sess-${counter}`, amr: [{ method: "totp" }] }),
|
|
17
|
+
token_type: "bearer",
|
|
18
|
+
expires_in: 3600,
|
|
19
|
+
expires_at: exp,
|
|
20
|
+
refresh_token: refreshToken ?? `rt-${process.pid}-${counter}-${Math.random().toString(36).slice(2)}`,
|
|
21
|
+
user: { id: `user-${persona}`, email: persona },
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createFakeSignIn({ nowMs = () => Date.now(), onRefresh, onMint } = {}) {
|
|
26
|
+
const live = new Map(); // refresh_token -> { persona, sessionId, aal }
|
|
27
|
+
const calls = { mint: 0, refresh: 0 };
|
|
28
|
+
return {
|
|
29
|
+
calls,
|
|
30
|
+
live,
|
|
31
|
+
downgrade(sessionId) {
|
|
32
|
+
for (const entry of live.values()) if (entry.sessionId === sessionId) entry.aal = "aal1";
|
|
33
|
+
},
|
|
34
|
+
revokeAll() {
|
|
35
|
+
live.clear();
|
|
36
|
+
},
|
|
37
|
+
async mint({ persona }) {
|
|
38
|
+
calls.mint += 1;
|
|
39
|
+
if (onMint) await onMint({ persona });
|
|
40
|
+
const session = fakeStoredSession({ persona, nowMs: nowMs() });
|
|
41
|
+
const sessionId = JSON.parse(Buffer.from(session.access_token.split(".")[1], "base64url")).session_id;
|
|
42
|
+
live.set(session.refresh_token, { persona, sessionId, aal: "aal2" });
|
|
43
|
+
return session;
|
|
44
|
+
},
|
|
45
|
+
async refresh({ storedSession, persona }) {
|
|
46
|
+
calls.refresh += 1;
|
|
47
|
+
if (onRefresh) {
|
|
48
|
+
const injected = await onRefresh({ storedSession, persona });
|
|
49
|
+
if (injected) return injected;
|
|
50
|
+
}
|
|
51
|
+
const entry = live.get(storedSession.refresh_token);
|
|
52
|
+
if (!entry) return { ok: false, transient: false, reason: "refresh_token_not_found" };
|
|
53
|
+
live.delete(storedSession.refresh_token);
|
|
54
|
+
if (entry.aal !== "aal2") return { ok: false, transient: false, reason: "aal_downgraded" };
|
|
55
|
+
const next = fakeStoredSession({ persona: entry.persona, sessionId: entry.sessionId, nowMs: nowMs() });
|
|
56
|
+
live.set(next.refresh_token, entry);
|
|
57
|
+
return { ok: true, storedSession: next };
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// BOT-2084 (lifted from supplyspark-ent ENT-5120) test helper: one OS process racing
|
|
2
|
+
// `take` against others on a shared pool dir.
|
|
3
|
+
import { createSessionPool } from "../pool.mjs";
|
|
4
|
+
import { SG_TARGET } from "./bindings.mjs";
|
|
5
|
+
import { fakeStoredSession } from "./fake-signin.mjs";
|
|
6
|
+
|
|
7
|
+
const [poolDir, persona, startAtMs] = process.argv.slice(2);
|
|
8
|
+
const pool = createSessionPool({
|
|
9
|
+
poolDir,
|
|
10
|
+
target: SG_TARGET,
|
|
11
|
+
signIn: {
|
|
12
|
+
mint: async () => {
|
|
13
|
+
throw new Error("workers never mint");
|
|
14
|
+
},
|
|
15
|
+
refresh: async () => ({ ok: true, storedSession: fakeStoredSession({ persona }) }),
|
|
16
|
+
},
|
|
17
|
+
logger: () => {},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Line up all workers on the same instant so the renames genuinely race.
|
|
21
|
+
const delay = Number(startAtMs) - Date.now();
|
|
22
|
+
if (delay > 0) await new Promise((resolve) => setTimeout(resolve, delay));
|
|
23
|
+
try {
|
|
24
|
+
const taken = await pool.take({ persona, owner: `worker-${process.pid}` });
|
|
25
|
+
process.stdout.write(JSON.stringify({ slotId: taken.slotId }));
|
|
26
|
+
} catch (error) {
|
|
27
|
+
process.stdout.write(JSON.stringify({ error: error.code ?? error.message }));
|
|
28
|
+
}
|