@botbuddy/cli 1.60.4 → 1.60.6
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 +4 -2
- package/src/commands.mjs +28 -0
- package/src/pw/args.mjs +17 -0
- package/src/pw/coordinator.mjs +5 -1
- package/src/pw/run.mjs +70 -9
- package/src/pw/staging-auth.mjs +371 -0
- package/src/staging-pool/binding.mjs +122 -0
- package/src/staging-pool/cli.mjs +482 -0
- package/src/staging-pool/config.mjs +107 -0
- package/src/staging-pool/errors.mjs +380 -0
- package/src/staging-pool/health.mjs +84 -0
- package/src/staging-pool/help.mjs +125 -0
- package/src/staging-pool/job.mjs +7 -0
- package/src/staging-pool/launchd.mjs +92 -0
- package/src/staging-pool/password-source.mjs +65 -0
- package/src/staging-pool/policy.mjs +186 -0
- package/src/staging-pool/pool.mjs +666 -0
- package/src/staging-pool/signin-gotrue-totp.mjs +198 -0
- package/src/staging-pool/state-file.mjs +166 -0
- package/src/staging-pool/totp-botbuddy.mjs +106 -0
|
Binary file
|
|
Binary file
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"release_version": "1.60.
|
|
3
|
-
"source_commit": "
|
|
4
|
-
"source_sha256": "
|
|
2
|
+
"release_version": "1.60.6",
|
|
3
|
+
"source_commit": "ae130a31e09d0752d2f395aac02d51fa89e0c93f",
|
|
4
|
+
"source_sha256": "7c1fe3ec9d1d28f25c665e4f607690dc512fabf0a9f1eebaf1c86e1611382edf",
|
|
5
5
|
"target": "darwin-arm64",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "2cd38dbe8411dd7499107065d3c526bd171f9edc37bc820aa79ab2c5cead27ec"
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"release_version": "1.60.
|
|
3
|
-
"source_commit": "
|
|
4
|
-
"source_sha256": "
|
|
2
|
+
"release_version": "1.60.6",
|
|
3
|
+
"source_commit": "ae130a31e09d0752d2f395aac02d51fa89e0c93f",
|
|
4
|
+
"source_sha256": "7c1fe3ec9d1d28f25c665e4f607690dc512fabf0a9f1eebaf1c86e1611382edf",
|
|
5
5
|
"target": "linux-x64",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "4d422182d63d0ee3ac38bf2823d255fd059ca5195efda355681e8f94137eaf7a"
|
|
7
7
|
}
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": 1,
|
|
3
|
-
"release_version": "1.60.
|
|
4
|
-
"source_commit": "
|
|
5
|
-
"source_sha256": "
|
|
3
|
+
"release_version": "1.60.6",
|
|
4
|
+
"source_commit": "ae130a31e09d0752d2f395aac02d51fa89e0c93f",
|
|
5
|
+
"source_sha256": "7c1fe3ec9d1d28f25c665e4f607690dc512fabf0a9f1eebaf1c86e1611382edf",
|
|
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": "2cd38dbe8411dd7499107065d3c526bd171f9edc37bc820aa79ab2c5cead27ec",
|
|
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": "4d422182d63d0ee3ac38bf2823d255fd059ca5195efda355681e8f94137eaf7a",
|
|
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.60.
|
|
3
|
+
"version": "1.60.6",
|
|
4
4
|
"description": "BotBuddy \u2014 Swarm coordination CLI for multi-agent workflows",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
"daemon/",
|
|
20
20
|
"daemon-release/",
|
|
21
21
|
"src/",
|
|
22
|
-
"!src/**/*.test.mjs"
|
|
22
|
+
"!src/**/*.test.mjs",
|
|
23
|
+
"!src/staging-pool/test-helpers/",
|
|
24
|
+
"!src/staging-pool/fixtures/"
|
|
23
25
|
],
|
|
24
26
|
"keywords": [
|
|
25
27
|
"mcp",
|
package/src/commands.mjs
CHANGED
|
@@ -25,6 +25,7 @@ import { VERSION } from "./version.mjs";
|
|
|
25
25
|
import { setupMcpKey, revokeMcpKey, resolveMcpConfigKey, resolveEnvVarName, DEFAULT_MCP_ENV_VAR, McpKeyError, McpKeyStoreError } from "./mcp-key.mjs";
|
|
26
26
|
import { readAgentBinding } from "./wait-profile.mjs";
|
|
27
27
|
import { runPw } from "./pw/run.mjs";
|
|
28
|
+
import { symptomHelp as stagingPoolSymptomHelp } from "./staging-pool/errors.mjs";
|
|
28
29
|
import { maybeWarnStale, cmdUpdate } from "./update-check.mjs";
|
|
29
30
|
import { createTelemetryOutbox } from "./telemetry-outbox.mjs";
|
|
30
31
|
import { attestTelemetryCredential, loadTelemetryIdentity, loadTelemetryLocation } from "./telemetry-config.mjs";
|
|
@@ -123,6 +124,8 @@ export async function run(argv, {
|
|
|
123
124
|
case "test-ingest": return cmdTestIngest(args);
|
|
124
125
|
case "wait": return runWait(args);
|
|
125
126
|
case "pw": return runPw(args);
|
|
127
|
+
// BOT-2084: the human-installed staging session pool broker.
|
|
128
|
+
case "staging-pool": return (await import("./staging-pool/cli.mjs")).runStagingPool(args);
|
|
126
129
|
case "mcp": return cmdMcp(args);
|
|
127
130
|
case "resources": return callTool("list_resources");
|
|
128
131
|
case "agents": return callTool("list_agents");
|
|
@@ -269,6 +272,16 @@ ${bold("BROWSER LANES")}
|
|
|
269
272
|
with --environment <env> or BB_PW_ENV)
|
|
270
273
|
pw --help Show lane usage
|
|
271
274
|
|
|
275
|
+
${bold("STAGING SIGN-IN")} (BOT-2084)
|
|
276
|
+
pw --environment staging <lane> auth --persona <alias>
|
|
277
|
+
Agents: sign a claimed lane in to staging as a seeded
|
|
278
|
+
persona, no password (${cyan("bb pw auth --help")})
|
|
279
|
+
staging-pool status [--check]
|
|
280
|
+
Pool health: one reason, who acts, the next command
|
|
281
|
+
staging-pool install|tick|mint-one|uninstall
|
|
282
|
+
Human only, from Terminal.app (${cyan("bb staging-pool --help")})
|
|
283
|
+
help symptom staging-pool Every refusal code with its cause and next command
|
|
284
|
+
|
|
272
285
|
${bold("OTHER")}
|
|
273
286
|
locks -m [--host name] Reserve typed local resources, including Playwright MCP lanes
|
|
274
287
|
evidence publish <file> Publish a local evidence statement → botbuddy/ready check-run
|
|
@@ -421,6 +434,13 @@ async function cmdToolHelp(args) {
|
|
|
421
434
|
: `Discovery failed: ${res.status} ${res.statusText}`);
|
|
422
435
|
}
|
|
423
436
|
console.log(formatDiscovery(await res.json(), { tool }));
|
|
437
|
+
if (!tool) console.log(toolCatalogFooter());
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// BOT-2084: local CLI commands are not MCP tools, so the catalog names where
|
|
441
|
+
// their help lives (kept to a few lines: the discovery help has a size budget).
|
|
442
|
+
export function toolCatalogFooter() {
|
|
443
|
+
return `\nLocal CLI commands (not MCP tools): ${cyan("bb pw auth --help")} staging sign-in for a lane · ${cyan("bb staging-pool --help")} the human-installed session pool · ${cyan("bb help symptom staging-pool")} their error codes.`;
|
|
424
444
|
}
|
|
425
445
|
|
|
426
446
|
// Keep the most common lifecycle recoveries local: an agent must be able to
|
|
@@ -451,6 +471,8 @@ ${bold("WAITING")}
|
|
|
451
471
|
|
|
452
472
|
${cyan("telemetry-unavailable")} A test launch has no test-run ID or wait command.
|
|
453
473
|
${cyan("command-run-stuck")} A local command receipt and remote lifecycle disagree.
|
|
474
|
+
${cyan("staging-pool")} Every bb staging-pool / bb pw auth error code (BOT-2084);
|
|
475
|
+
${cyan("bb help symptom <code>")} for one, e.g. pool_empty.
|
|
454
476
|
|
|
455
477
|
Choose the symptom printed by the command. If neither applies, preserve the
|
|
456
478
|
original command result and receipt, then report the exact error and command ID.`;
|
|
@@ -506,6 +528,12 @@ ${bold("Boundary")}
|
|
|
506
528
|
Command-run status is agent-owned. A different agent may be denied access; that
|
|
507
529
|
denial is not evidence that the run is missing or terminal.`;
|
|
508
530
|
|
|
531
|
+
|
|
532
|
+
// BOT-2084: the staging session pool's error codes (cli/src/staging-pool/errors.mjs).
|
|
533
|
+
if (topic.startsWith("symptom ")) {
|
|
534
|
+
const local = stagingPoolSymptomHelp(topic.slice("symptom ".length));
|
|
535
|
+
if (local) return local;
|
|
536
|
+
}
|
|
509
537
|
return null;
|
|
510
538
|
}
|
|
511
539
|
|
package/src/pw/args.mjs
CHANGED
|
@@ -61,6 +61,23 @@ export function planInvocation(argv, env = process.env) {
|
|
|
61
61
|
if (argv.length !== 3 || !["pass", "fail"].includes(argv[2])) throw new Error("bb-pw: usage: bb-pw <lane> verdict pass|fail");
|
|
62
62
|
return { scope: "lane", lane, session: `lane-${lane}`, verb, mode: "verdict", verdict: argv[2] };
|
|
63
63
|
}
|
|
64
|
+
// BOT-2084: `auth --persona <slug>` is a LOCAL wrapper verb (never forwarded to
|
|
65
|
+
// Playwright). Wrapper flags stay before the lane; only --persona follows it.
|
|
66
|
+
if (verb === "auth") {
|
|
67
|
+
const authArgs = argv.slice(2);
|
|
68
|
+
const strayAuth = authArgs.find((value) => credentialFlagOf(value));
|
|
69
|
+
if (strayAuth) throw misplacedCredentialFlag(strayAuth);
|
|
70
|
+
const usage = "bb-pw: usage: bb pw --environment staging <lane> auth --persona <slug>";
|
|
71
|
+
let persona = null;
|
|
72
|
+
for (let i = 0; i < authArgs.length; i++) {
|
|
73
|
+
const token = String(authArgs[i]);
|
|
74
|
+
if (token === "--persona" && persona === null && authArgs[i + 1] !== undefined && !String(authArgs[i + 1]).startsWith("--")) { persona = String(authArgs[++i]); continue; }
|
|
75
|
+
if (token.startsWith("--persona=") && persona === null && token.length > "--persona=".length) { persona = token.slice("--persona=".length); continue; }
|
|
76
|
+
throw new Error(`${usage} (unexpected '${token}')`);
|
|
77
|
+
}
|
|
78
|
+
if (!persona) throw new Error(`${usage} (--persona is required)`);
|
|
79
|
+
return { scope: "lane", lane, session: `lane-${lane}`, verb, mode: "auth", persona };
|
|
80
|
+
}
|
|
64
81
|
if (verb === "status") {
|
|
65
82
|
// `status` has no workload, so — like the global verbs — there is no `--`
|
|
66
83
|
// exemption: a credential flag in ANY position (incl. after `--`) is misplaced
|
package/src/pw/coordinator.mjs
CHANGED
|
@@ -18,7 +18,11 @@ function mcpCaller(authHeader, fetchImpl) {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
function laneCoordinator(kind, call, agentId) {
|
|
21
|
-
return { kind,
|
|
21
|
+
return { kind,
|
|
22
|
+
// BOT-2084: the agent this credential authenticates as (whoami), for checks that
|
|
23
|
+
// need the exact holder when no local agent id is known (an exported session token).
|
|
24
|
+
async whoami() { const identity = await call("whoami", {}); return typeof identity?.agent?.id === "string" ? identity.agent.id : null; },
|
|
25
|
+
async status({ host, slot }) { const result = await call("list_resources", { host, subtype: "playwright_lane" }); const list = Array.isArray(result) ? result : result.resources ?? []; const resource = list.find((item) => item.name === `playwright_lane:${host}:${slot}` || String(item.slot) === String(slot)); /* BOT-1490: canonical_host is a TOP-LEVEL field the server echoes even on an empty page (not-held lane); owner_is_caller is per-row. Older servers send neither → null. */ const canonicalHost = (result && !Array.isArray(result) ? result.canonical_host : null) ?? null; return resource ? { held: resource.status !== "free", heldBy: resource.owner_agent_id ?? null, heldByName: resource.agents?.name ?? null, host: resource.host ?? null, name: resource.name ?? null, slot: resource.slot ?? null, canonicalHost, ownerIsCaller: resource.owner_is_caller ?? null } : { held: false, heldBy: null, heldByName: null, host: null, name: null, slot: null, canonicalHost, ownerIsCaller: null }; }, agentId };
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
// BOT-1572/1582: authenticate lock verification with a per-session `bb_agent_` token —
|
package/src/pw/run.mjs
CHANGED
|
@@ -10,11 +10,12 @@ import { loadConfig, getConfig } from "../config.mjs";
|
|
|
10
10
|
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
|
+
import * as stagingAuth from "./staging-auth.mjs";
|
|
13
14
|
// BOT-1488: canonicalize the raw hostname the SAME way acquire_resources does
|
|
14
15
|
// server-side, so the lane name bb-pw builds/matches/prints is the one the lock
|
|
15
16
|
// kernel actually stored ("jonos-mbp:8", not "Jonos-MBP.localdomain:8").
|
|
16
17
|
const hostFor = (env) => canonicalizeHostString(env.PLAYWRIGHT_MCP_HOST || env.HOSTNAME || os.hostname());
|
|
17
|
-
function help(out) { out.write("Usage: pw [--tenant <slug>] [--session-id <id>] [--agent-session-token <token>] [--environment <env>] <lane> <verb> [args…]\n\nAliases: bb-pw <lane> <verb> [args…] · botbuddy pw <lane> <verb> [args…]\n (all three drive the same lock-gated Playwright lane)\n\n--session-id <id> accept a lane held by this arming-session agent id (from\n register_agent); defaults to $BOTBUDDY_SESSION_ID, then the\n id saved by `botbuddy register`.\n--agent-session-token <token> session credential (env $BOTBUDDY_AGENT_SESSION_TOKEN);\n --agent-key / --session-token are one-release legacy aliases.\n--tenant <slug> accepted before the lane but currently INERT — the tenant\n comes from your resolved credential, so bb pw does not act on\n this flag for session/lane auth yet (server-validated tenant\n selection is tracked in BOT-1732).\n\n--environment <local|preview|staging|production> the environment a TRACKED\n browser session is exercising (env $BB_PW_ENV when absent; the\n flag wins). Required when a session starts in a repository\n with .botbuddy/config.json; persisted for the session, and a\n later disagreeing value is refused.\n\nBrowser-acceptance telemetry (BOT-1318): each lane browser session (open →\nclose) is reported once as a browser_acceptance execution with wall/active/\ncommand time, per-action results and a verdict. Every action's result is\nuploaded before the command returns; offline, events queue locally and drain\nlater. Values you type/fill, secrets, query strings and raw output are never\nrecorded. BB_PW_NO_LOCK=1 stays local and untracked.\n bb-pw <lane> verdict pass|fail record the acceptance verdict (local; never\n sent to Playwright). Without it a session\n closes as not_asserted.\n bb-pw <lane> status shows browser_session: source_run_id,\n execution_id, delivery, environment, verdict.\n\nCredential flags (--tenant / --session-id / --agent-session-token) and\n--environment go BEFORE the lane. Placed after the lane they are refused with guidance (they are not\nPlaywright arguments, and a --tenant after the lane cannot be safely honored).\n\nFile upload (BOT-1702) — works on a hidden <input type=file>:\n bb-pw <lane> upload <selector> <path...>\n bb-pw <lane> upload <selector> --inline name=<n>,mime=<m>,size=<bytes>\n bb-pw <lane> upload <selector> --inline name=<n>,mime=<m>,bytes=@base64:<b64>\n bb-pw <lane> upload <selector> --inline name=<n>,mime=<m>,text=<str>\n --inline repeats; builds synthetic files in memory (no workstation FS).\n <selector> also accepts --testid/--role/--label/--text like click/fill.\n\nNetwork mocking (BOT-1702) — routes persist on the lane across invocations:\n bb-pw <lane> route <url-glob> stall hold the request pending\n (screenshot the loading state)\n bb-pw <lane> route <url-glob> abort [--error <code>] fail the request (default: failed)\n bb-pw <lane> route <url-glob> fulfill --status <n> [--content-type <ct>]\n [--body <str|@base64:..|@file:PATH>]\n bb-pw <lane> unroute [<url-glob>] release one route, or all\n bb-pw <lane> route-list list the routes owned on this lane\n (offline mode already exists upstream: bb-pw <lane> network-state-set offline)\n"); }
|
|
18
|
+
function help(out) { out.write("Usage: pw [--tenant <slug>] [--session-id <id>] [--agent-session-token <token>] [--environment <env>] <lane> <verb> [args…]\n\nAliases: bb-pw <lane> <verb> [args…] · botbuddy pw <lane> <verb> [args…]\n (all three drive the same lock-gated Playwright lane)\n\n--session-id <id> accept a lane held by this arming-session agent id (from\n register_agent); defaults to $BOTBUDDY_SESSION_ID, then the\n id saved by `botbuddy register`.\n--agent-session-token <token> session credential (env $BOTBUDDY_AGENT_SESSION_TOKEN);\n --agent-key / --session-token are one-release legacy aliases.\n--tenant <slug> accepted before the lane but currently INERT — the tenant\n comes from your resolved credential, so bb pw does not act on\n this flag for session/lane auth yet (server-validated tenant\n selection is tracked in BOT-1732).\n\n--environment <local|preview|staging|production> the environment a TRACKED\n browser session is exercising (env $BB_PW_ENV when absent; the\n flag wins). Required when a session starts in a repository\n with .botbuddy/config.json; persisted for the session, and a\n later disagreeing value is refused.\n\nBrowser-acceptance telemetry (BOT-1318): each lane browser session (open →\nclose) is reported once as a browser_acceptance execution with wall/active/\ncommand time, per-action results and a verdict. Every action's result is\nuploaded before the command returns; offline, events queue locally and drain\nlater. Values you type/fill, secrets, query strings and raw output are never\nrecorded. BB_PW_NO_LOCK=1 stays local and untracked.\n bb-pw <lane> verdict pass|fail record the acceptance verdict (local; never\n sent to Playwright). Without it a session\n closes as not_asserted.\n bb-pw <lane> status shows browser_session: source_run_id,\n execution_id, delivery, environment, verdict.\n\nCredential flags (--tenant / --session-id / --agent-session-token) and\n--environment go BEFORE the lane. Placed after the lane they are refused with guidance (they are not\nPlaywright arguments, and a --tenant after the lane cannot be safely honored).\n\nFile upload (BOT-1702) — works on a hidden <input type=file>:\n bb-pw <lane> upload <selector> <path...>\n bb-pw <lane> upload <selector> --inline name=<n>,mime=<m>,size=<bytes>\n bb-pw <lane> upload <selector> --inline name=<n>,mime=<m>,bytes=@base64:<b64>\n bb-pw <lane> upload <selector> --inline name=<n>,mime=<m>,text=<str>\n --inline repeats; builds synthetic files in memory (no workstation FS).\n <selector> also accepts --testid/--role/--label/--text like click/fill.\n\nNetwork mocking (BOT-1702) — routes persist on the lane across invocations:\n bb-pw <lane> route <url-glob> stall hold the request pending\n (screenshot the loading state)\n bb-pw <lane> route <url-glob> abort [--error <code>] fail the request (default: failed)\n bb-pw <lane> route <url-glob> fulfill --status <n> [--content-type <ct>]\n [--body <str|@base64:..|@file:PATH>]\n bb-pw <lane> unroute [<url-glob>] release one route, or all\n bb-pw <lane> route-list list the routes owned on this lane\n (offline mode already exists upstream: bb-pw <lane> network-state-set offline)\n\nStaging sign-in (BOT-2084) — agents sign a lane in to staging without a password:\n bb pw --environment staging <lane> auth --persona <alias>\n Installs a session from the human-installed staging session pool into this lane only.\n Prerequisites, example, output and every refusal: bb pw auth --help\n bb setup / bb doctor restore YOUR BotBuddy agent session; they never sign in to the\n application under test.\n\nSaved browser state (state-save / state-load) is forwarded to Playwright. State files are\nSECRET-BEARING and are not a staging sign-in path: use auth above.\n"); }
|
|
18
19
|
// Redact LONGEST secrets first: replacing a short value that is a substring of a
|
|
19
20
|
// longer secret (e.g. a filename that also appears inside the payload's base64)
|
|
20
21
|
// would otherwise break the longer match and leave the remainder recoverable
|
|
@@ -67,7 +68,7 @@ async function gate({ env, host, lane, deps }) {
|
|
|
67
68
|
coordinator = createSessionTokenCoordinator({ token, fetchImpl: deps.fetch });
|
|
68
69
|
}
|
|
69
70
|
if (coordinator.kind === "unverified") return { allowed: false, message: "bb-pw: BotBuddy credential is unusable. Re-register the agent, or set BB_PW_NO_LOCK=1 for local-only work." };
|
|
70
|
-
const selfAgentIds = new Set([sessionAgentId, registeredAgentId, coordinator.agentId].filter(Boolean));
|
|
71
|
+
const selfAgentIds = new Set([sessionAgentId, registeredAgentId, coordinator.agentId, deps.credentialAgentId].filter(Boolean));
|
|
71
72
|
const callerId = coordinator.agentId ?? sessionAgentId ?? registeredAgentId ?? "botbuddy";
|
|
72
73
|
const laneName = `playwright_lane:${host}:${lane}`;
|
|
73
74
|
try {
|
|
@@ -85,15 +86,20 @@ async function gate({ env, host, lane, deps }) {
|
|
|
85
86
|
// the holder to the caller's owner (BOT-1490 owner_is_caller — server-authoritative,
|
|
86
87
|
// closing the shared-hostname gap without trusting a local file). The local paths
|
|
87
88
|
// stay as fallbacks for older servers that do not send owner_is_caller.
|
|
88
|
-
|
|
89
|
+
// BOT-2084: report HOW the holder matched. `auth` (staging session issuance)
|
|
90
|
+
// requires the exact agent ("self"), never only a same-owner match ("owner").
|
|
91
|
+
// callerIds/holder let staging-auth cleanup touch only what this caller installed.
|
|
92
|
+
const callerIds = [...selfAgentIds];
|
|
93
|
+
if (status.held && selfAgentIds.has(status.heldBy)) return { allowed: true, coordinator, canonicalHost, holderMatch: "self", holder: status.heldBy, callerIds };
|
|
94
|
+
if (status.held && status.ownerIsCaller === true) return { allowed: true, coordinator, canonicalHost, holderMatch: "owner", holder: status.heldBy ?? null, callerIds };
|
|
89
95
|
// Distinguish an expired/free lane from one another operator holds so the
|
|
90
96
|
// operator knows whether to re-acquire vs. wait for a handover (BOT-660). Name it
|
|
91
97
|
// by the server's canonical host so a not-held message matches the dashboard even
|
|
92
98
|
// when local normalization cannot reach the alias-collapsed key (BOT-1490 AC-5).
|
|
93
|
-
if (!status.held) return { allowed: false, message: `bb-pw: lane lock ${foundLaneName} is not held (caller ${callerId}). Acquire it via BotBuddy first, or set BB_PW_NO_LOCK=1 for local-only work.` };
|
|
99
|
+
if (!status.held) return { allowed: false, reason: "not_held", canonicalHost, callerIds, coordinator, message: `bb-pw: lane lock ${foundLaneName} is not held (caller ${callerId}). Acquire it via BotBuddy first, or set BB_PW_NO_LOCK=1 for local-only work.` };
|
|
94
100
|
const holder = status.heldBy ?? "unknown";
|
|
95
101
|
const holderName = status.heldByName ? ` (${status.heldByName})` : "";
|
|
96
|
-
return { allowed: false, message: `bb-pw: lane lock ${foundLaneName} is held by ${holder}${holderName}, not you (caller ${callerId}). Acquire it first or set BB_PW_NO_LOCK=1 for local-only work.` };
|
|
102
|
+
return { allowed: false, reason: "held_by_other", canonicalHost, callerIds, coordinator, message: `bb-pw: lane lock ${foundLaneName} is held by ${holder}${holderName}, not you (caller ${callerId}). Acquire it first or set BB_PW_NO_LOCK=1 for local-only work.` };
|
|
97
103
|
} catch (error) {
|
|
98
104
|
return { allowed: false, errorCode: error?.code ?? null, message: `bb-pw: could not verify lane lock ${laneName} (${error.message}). Set BB_PW_NO_LOCK=1 for local-only work.` };
|
|
99
105
|
}
|
|
@@ -183,8 +189,20 @@ function consumeWrapperFlags(list, deps, start = 0) {
|
|
|
183
189
|
return { deps, rest };
|
|
184
190
|
}
|
|
185
191
|
|
|
192
|
+
// The non-secret values a staging-auth refusal names in its next command.
|
|
193
|
+
const authVars = (plan) => ({ lane: plan.lane, persona: plan.persona });
|
|
194
|
+
|
|
195
|
+
// `bb pw auth --help` / `bb pw [flags] <lane> auth --help`: auth's own help, before
|
|
196
|
+
// any credential, lock or daemon call.
|
|
197
|
+
function wantsAuthHelp(args) {
|
|
198
|
+
const at = args.indexOf("auth");
|
|
199
|
+
if (at === -1 || !(at === 0 || /^[0-9]+$/.test(args[at - 1]))) return false;
|
|
200
|
+
return args.slice(at + 1).some((token) => token === "--help" || token === "-h");
|
|
201
|
+
}
|
|
202
|
+
|
|
186
203
|
async function runPwInner(argv, deps = {}) {
|
|
187
204
|
const env = deps.env ?? process.env, stdout = deps.stdout ?? process.stdout, stderr = deps.stderr ?? process.stderr; let args = [...argv]; if (["--help", "-h"].includes(args[0])) { help(stdout); return 0; }
|
|
205
|
+
if (wantsAuthHelp(args)) { stdout.write(`${(await import("../staging-pool/help.mjs")).PW_AUTH_HELP}\n`); return 0; }
|
|
188
206
|
if (["--version", "-v"].includes(args[0])) { stdout.write(`${versionLine()}\n`); return 0; }
|
|
189
207
|
// BOT-1701 (AC-6): one stderr nudge for a tier-3 legacy flag/env alias. bb pw
|
|
190
208
|
// only recognizes credential flags as LEADING options (the while-loop below);
|
|
@@ -206,6 +224,14 @@ async function runPwInner(argv, deps = {}) {
|
|
|
206
224
|
// `--tenant` lane fallback is BOT-1732.
|
|
207
225
|
{ const consumed = consumeWrapperFlags(args, deps, 0); if (consumed.error) { stderr.write(`bb-pw: ${consumed.error} needs a value\n`); return 2; } deps = consumed.deps; args = consumed.rest; }
|
|
208
226
|
let plan; try { plan = planInvocation(args, env); } catch (error) { stderr.write(`${error.message}\n`); return 2; }
|
|
227
|
+
// BOT-2084 (#1068 run.mjs:110): auth only ever issues a STAGING session into a
|
|
228
|
+
// lock-gated lane, so refuse any other resolved environment — and the unlocked
|
|
229
|
+
// local mode — before any credential, lock or issuer call.
|
|
230
|
+
if (plan.mode === "auth") {
|
|
231
|
+
const authEnvironment = resolveEnvironment({ flag: deps.environment ?? null, env });
|
|
232
|
+
if (authEnvironment.error || authEnvironment.environment !== "staging") return stagingAuth.refuse({ stdout, stderr }, "auth_requires_staging_environment", 2, {}, authVars(plan));
|
|
233
|
+
if (env.BB_PW_NO_LOCK === "1") return stagingAuth.refuse({ stdout, stderr }, "auth_requires_lane_lock", 3, {}, authVars(plan));
|
|
234
|
+
}
|
|
209
235
|
const { spawnExec, socketRun } = deps.daemon ?? await import("./daemon.mjs");
|
|
210
236
|
if (plan.scope === "global") { if (plan.mode === "reap") { await (deps.reap ?? (await import("./reap.mjs")).reap)({ env, stdout }); return 0; } if (env.BB_PW_NO_LOCK !== "1") { stderr.write("bb-pw: close-all and kill-all require BB_PW_NO_LOCK=1 because they can affect lanes you do not own.\n"); return 3; } return spawnExec(plan, env); }
|
|
211
237
|
// Local-only lanes explicitly opt out of lock ownership, so they also need no
|
|
@@ -215,7 +241,7 @@ async function runPwInner(argv, deps = {}) {
|
|
|
215
241
|
try {
|
|
216
242
|
const credential = await (deps.resolveCredential ?? resolveAgentSessionCredential)({ argv, env, cwd: deps.cwd ?? process.cwd(), sessionId: deps.sessionId ?? env.BOTBUDDY_SESSION_ID ?? null, flagToken: null });
|
|
217
243
|
managedCredential = credential;
|
|
218
|
-
deps = { ...deps, sessionToken: credential.token, sessionId: deps.sessionId ?? credential.sessionId ?? null };
|
|
244
|
+
deps = { ...deps, sessionToken: credential.token, sessionId: deps.sessionId ?? credential.sessionId ?? null, credentialAgentId: credential.agentId ?? null };
|
|
219
245
|
} catch (error) {
|
|
220
246
|
stderr.write(selfHealErrorLine(error)); return 3;
|
|
221
247
|
}
|
|
@@ -232,7 +258,7 @@ async function runPwInner(argv, deps = {}) {
|
|
|
232
258
|
managedCredential = credential;
|
|
233
259
|
// Keep an explicit --session-id holder authoritative over the freshly minted
|
|
234
260
|
// one, same precedence as the initial resolution (Codex).
|
|
235
|
-
deps = { ...deps, sessionToken: credential.token, sessionId: deps.sessionId ?? credential.sessionId ?? null };
|
|
261
|
+
deps = { ...deps, sessionToken: credential.token, sessionId: deps.sessionId ?? credential.sessionId ?? null, credentialAgentId: credential.agentId ?? null };
|
|
236
262
|
} catch (error) {
|
|
237
263
|
// BOT-1682: the renewal after a revoked cache can also fail self-heal; fail
|
|
238
264
|
// closed with a clear message rather than letting the error escape to exit 1.
|
|
@@ -240,7 +266,21 @@ async function runPwInner(argv, deps = {}) {
|
|
|
240
266
|
}
|
|
241
267
|
auth = await gate({ env, host, lane: plan.lane, deps });
|
|
242
268
|
}
|
|
243
|
-
|
|
269
|
+
// BOT-2084 staging-auth hooks share the lane's daemon and the gate-verified
|
|
270
|
+
// credential; the trust bridge decides how that credential reaches Staging.
|
|
271
|
+
const daemonModule = deps.daemon?.resolveSession ? null : await import("./daemon.mjs");
|
|
272
|
+
const authCtx = { plan, host: auth.canonicalHost ?? host, env, deps, stdout, stderr, socketRun, resolveSession: deps.daemon?.resolveSession ?? daemonModule.resolveSession,
|
|
273
|
+
holder: auth.holder ?? null, callerIds: auth.callerIds ?? [],
|
|
274
|
+
// An exported session token carries no local agent id: ask the server who it is.
|
|
275
|
+
resolveCallerId: typeof auth.coordinator?.whoami === "function" ? () => auth.coordinator.whoami() : null,
|
|
276
|
+
credential: { token: deps.sessionToken ?? readAgentSessionTokenEnv(env) ?? null, agentId: deps.credentialAgentId ?? null, sessionId: deps.sessionId ?? null } };
|
|
277
|
+
if (!auth.allowed) {
|
|
278
|
+
// A lane that is provably no longer ours (not a transient verify error):
|
|
279
|
+
// clear the session we installed and revoke its custody (AC-3).
|
|
280
|
+
if (auth.reason === "not_held" || auth.reason === "held_by_other") await stagingAuth.onLaneAuthorityLost(authCtx).catch(() => {});
|
|
281
|
+
if (plan.mode === "auth" && (auth.reason === "not_held" || auth.reason === "held_by_other")) return stagingAuth.refuse({ stdout, stderr }, "lane_not_claimed", 3, { detail: auth.message }, authVars(plan));
|
|
282
|
+
stderr.write(`${auth.message}\n`); return 3;
|
|
283
|
+
}
|
|
244
284
|
// Report the host the lock service actually stored (alias-collapsed), so the
|
|
245
285
|
// status view and lane telemetry match the server, not the local string (AC-4).
|
|
246
286
|
const effHost = auth.canonicalHost ?? host;
|
|
@@ -259,6 +299,19 @@ async function runPwInner(argv, deps = {}) {
|
|
|
259
299
|
const envResolution = resolveEnvironment({ flag: deps.environment ?? null, env });
|
|
260
300
|
if (tracker && envResolution.error) { stderr.write(`${envResolution.error}\n`); return 2; }
|
|
261
301
|
const environment = envResolution.environment ?? null;
|
|
302
|
+
if (plan.mode === "auth") {
|
|
303
|
+
let holderMatch = auth.holderMatch;
|
|
304
|
+
if (holderMatch === "owner" && auth.holder && authCtx.resolveCallerId) {
|
|
305
|
+
const me = await authCtx.resolveCallerId().catch(() => null);
|
|
306
|
+
if (me && me === auth.holder) { holderMatch = "self"; authCtx.callerIds = [...authCtx.callerIds, me]; }
|
|
307
|
+
}
|
|
308
|
+
if (holderMatch !== "self") return stagingAuth.refuse({ stdout, stderr }, "exact_lane_holder_required", 3, {}, authVars(plan));
|
|
309
|
+
if (tracker) {
|
|
310
|
+
const browserSession = await tracker.status();
|
|
311
|
+
if (browserSession.active && browserSession.environment && browserSession.environment !== "staging") return stagingAuth.refuse({ stdout, stderr }, "browser_session_not_staging", 2, {}, authVars(plan));
|
|
312
|
+
}
|
|
313
|
+
return stagingAuth.runStagingAuth(authCtx);
|
|
314
|
+
}
|
|
262
315
|
if (plan.mode === "status") {
|
|
263
316
|
const lock = auth.coordinator?.status ? await auth.coordinator.status({ host: effHost, slot: plan.lane }) : null;
|
|
264
317
|
const browserSession = tracker ? await tracker.status() : { tracked: false, active: false };
|
|
@@ -275,6 +328,9 @@ async function runPwInner(argv, deps = {}) {
|
|
|
275
328
|
if (result.error) { stderr.write(`${result.error}\n`); return 2; }
|
|
276
329
|
reportDegradation(stderr, result.delivery); return 0;
|
|
277
330
|
}
|
|
331
|
+
// BOT-2084: crash cleanup and renewal-before-sweep for a lane carrying a
|
|
332
|
+
// staging custody; a failed renewal stops the command (AC-3).
|
|
333
|
+
{ const stop = await stagingAuth.beforeLaneCommand(authCtx); if (stop !== null) return stop; }
|
|
278
334
|
// Translate verbs ALWAYS run via the socket (even a small, non-secret one), so
|
|
279
335
|
// run.mjs — not an inherited-stdio spawnExec — owns their output and can strip
|
|
280
336
|
// the internal code echo; socketRun falls back to execArgv.slice(1) as its args
|
|
@@ -291,10 +347,15 @@ async function runPwInner(argv, deps = {}) {
|
|
|
291
347
|
if (inspect) { if (plan.fresh) await socketRun({ ...plan, socketArgs: ["snapshot"] }, env).catch(() => {}); const result = await socketRun({ ...plan, socketArgs: plan.socketArgs ?? plan.execArgv.slice(1) }, env); if (result.text) { const text = suppressEcho ? stripCodeEcho(result.text) : result.text; stdout.write(`${redact(text, plan.secretValues)}\n`); } if (!result.ok) { let errText = plan.targetKind === "ref" && isStaleRefError(result.error) ? staleRefRemediation(plan.target) : result.error; if (suppressEcho) errText = stripCodeEcho(errText); stderr.write(`${redact(errText, plan.secretValues)}\n`); } return { code: result.ok ? 0 : 1, errorText: result.ok ? null : String(result.error ?? "") }; }
|
|
292
348
|
const code = await spawnExec(plan, env); return { code, errorText: code === 0 ? null : `exit ${code}` };
|
|
293
349
|
};
|
|
294
|
-
if (!tracker)
|
|
350
|
+
if (!tracker) {
|
|
351
|
+
const result = await execute();
|
|
352
|
+
if (plan.verb === "close") await stagingAuth.afterLaneClose(authCtx);
|
|
353
|
+
return result.code;
|
|
354
|
+
}
|
|
295
355
|
const probe = async () => { const resolver = deps.daemon?.resolveSession ?? (await import("./daemon.mjs")).resolveSession; const { session } = await resolver(plan.session, env); return { alive: Boolean(session), generation: session?.socketPath ?? null }; };
|
|
296
356
|
if (plan.verb === "close") {
|
|
297
357
|
const closed = await tracker.close({ environment, probe, run: execute });
|
|
358
|
+
await stagingAuth.afterLaneClose(authCtx);
|
|
298
359
|
if (closed.error) { stderr.write(`${closed.error}\n`); return 2; }
|
|
299
360
|
reportDegradation(stderr, closed.delivery); return closed.code;
|
|
300
361
|
}
|