@botbuddy/cli 1.60.7 → 1.60.8
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/pw/args.mjs +6 -2
- package/src/pw/run.mjs +2 -1
- package/src/pw/staging-auth.mjs +20 -12
- package/src/staging-pool/binding.mjs +25 -40
- package/src/staging-pool/cli.mjs +197 -108
- package/src/staging-pool/config.mjs +4 -3
- package/src/staging-pool/errors.mjs +54 -33
- package/src/staging-pool/help.mjs +31 -22
- package/src/staging-pool/registry.mjs +79 -0
- package/src/staging-pool/checkouts.mjs +0 -120
|
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.8",
|
|
3
|
+
"source_commit": "057670de6f7ac5dc8e8e673dab13a317aef876e9",
|
|
4
|
+
"source_sha256": "3678a4ff49dda524b2b97e5a9d57249b64041f587da4db0c936b2f650647c6ec",
|
|
5
5
|
"target": "darwin-arm64",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "234246b7e1b8634420dbe2bceba35a4efa51a0ade206b09882f9c80a0aa66bfb"
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"release_version": "1.60.
|
|
3
|
-
"source_commit": "
|
|
4
|
-
"source_sha256": "
|
|
2
|
+
"release_version": "1.60.8",
|
|
3
|
+
"source_commit": "057670de6f7ac5dc8e8e673dab13a317aef876e9",
|
|
4
|
+
"source_sha256": "3678a4ff49dda524b2b97e5a9d57249b64041f587da4db0c936b2f650647c6ec",
|
|
5
5
|
"target": "linux-x64",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "c29069dc84ea8273ea4b560816eb130a82dfbba083a6d8149edecd8cc85b534c"
|
|
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.8",
|
|
4
|
+
"source_commit": "057670de6f7ac5dc8e8e673dab13a317aef876e9",
|
|
5
|
+
"source_sha256": "3678a4ff49dda524b2b97e5a9d57249b64041f587da4db0c936b2f650647c6ec",
|
|
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": "234246b7e1b8634420dbe2bceba35a4efa51a0ade206b09882f9c80a0aa66bfb",
|
|
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": "c29069dc84ea8273ea4b560816eb130a82dfbba083a6d8149edecd8cc85b534c",
|
|
19
19
|
"mode": "0755"
|
|
20
20
|
}
|
|
21
21
|
}
|
package/package.json
CHANGED
package/src/pw/args.mjs
CHANGED
|
@@ -67,16 +67,20 @@ export function planInvocation(argv, env = process.env) {
|
|
|
67
67
|
const authArgs = argv.slice(2);
|
|
68
68
|
const strayAuth = authArgs.find((value) => credentialFlagOf(value));
|
|
69
69
|
if (strayAuth) throw misplacedCredentialFlag(strayAuth);
|
|
70
|
-
const usage = "bb-pw: usage: bb pw --environment staging <lane> auth --persona <slug>";
|
|
70
|
+
const usage = "bb-pw: usage: bb pw --environment staging <lane> auth --persona <slug> [--pool <pool_id>]";
|
|
71
71
|
let persona = null;
|
|
72
|
+
let pool = null;
|
|
72
73
|
for (let i = 0; i < authArgs.length; i++) {
|
|
73
74
|
const token = String(authArgs[i]);
|
|
74
75
|
if (token === "--persona" && persona === null && authArgs[i + 1] !== undefined && !String(authArgs[i + 1]).startsWith("--")) { persona = String(authArgs[++i]); continue; }
|
|
75
76
|
if (token.startsWith("--persona=") && persona === null && token.length > "--persona=".length) { persona = token.slice("--persona=".length); continue; }
|
|
77
|
+
// BOT-2648: which installed pool, when several are installed.
|
|
78
|
+
if (token === "--pool" && pool === null && authArgs[i + 1] !== undefined && /^[a-z0-9][a-z0-9-]{0,63}$/.test(String(authArgs[i + 1]))) { pool = String(authArgs[++i]); continue; }
|
|
79
|
+
if (token.startsWith("--pool=") && pool === null && /^[a-z0-9][a-z0-9-]{0,63}$/.test(token.slice("--pool=".length))) { pool = token.slice("--pool=".length); continue; }
|
|
76
80
|
throw new Error(`${usage} (unexpected '${token}')`);
|
|
77
81
|
}
|
|
78
82
|
if (!persona) throw new Error(`${usage} (--persona is required)`);
|
|
79
|
-
return { scope: "lane", lane, session: `lane-${lane}`, verb, mode: "auth", persona };
|
|
83
|
+
return { scope: "lane", lane, session: `lane-${lane}`, verb, mode: "auth", persona, pool };
|
|
80
84
|
}
|
|
81
85
|
if (verb === "status") {
|
|
82
86
|
// `status` has no workload, so — like the global verbs — there is no `--`
|
package/src/pw/run.mjs
CHANGED
|
@@ -244,7 +244,8 @@ async function runPwInner(argv, deps = {}) {
|
|
|
244
244
|
try {
|
|
245
245
|
const credential = await (deps.resolveCredential ?? resolveAgentSessionCredential)({ argv, env, cwd: deps.cwd ?? process.cwd(), sessionId: deps.sessionId ?? env.BOTBUDDY_SESSION_ID ?? null, flagToken: null });
|
|
246
246
|
managedCredential = credential;
|
|
247
|
-
|
|
247
|
+
// BOT-2648 (Codex P2, #1323): the credential's tenant picks the installed staging pool.
|
|
248
|
+
deps = { ...deps, sessionToken: credential.token, sessionId: deps.sessionId ?? credential.sessionId ?? null, credentialAgentId: credential.agentId ?? null, credentialTenant: credential.identity?.tenant ?? null };
|
|
248
249
|
} catch (error) {
|
|
249
250
|
stderr.write(selfHealErrorLine(error)); return 3;
|
|
250
251
|
}
|
package/src/pw/staging-auth.mjs
CHANGED
|
@@ -273,20 +273,28 @@ export async function runStagingAuth({ plan, host, env, deps, socketRun, resolve
|
|
|
273
273
|
const poolModule = deps.stagingPoolModule ?? await loadPool();
|
|
274
274
|
const poolOptions = deps.stagingPool ?? {};
|
|
275
275
|
const cwd = deps.cwd ?? process.cwd();
|
|
276
|
-
const
|
|
277
|
-
|
|
276
|
+
const authLine = `bb pw --environment staging ${plan.lane} auth --persona ${plan.persona}`;
|
|
277
|
+
// The lane credential's tenant (resolved by run.mjs), else an explicit hint.
|
|
278
|
+
const tenantHint = deps.credentialTenant ?? deps.tenant ?? env.BOTBUDDY_TENANT ?? null;
|
|
279
|
+
const vars = { lane: plan.lane, persona: plan.persona, ticketFlag: ticketFlagFor(cwd), consequence: "bb pw auth has no pool to take a session from" };
|
|
280
|
+
// BOT-2648: the pool comes from the installed pool home (--pool, the only one,
|
|
281
|
+
// or the one for this tenant), never from the working directory.
|
|
282
|
+
let ctxPool;
|
|
278
283
|
try {
|
|
279
|
-
|
|
284
|
+
ctxPool = poolModule.resolveInstalledPool({ ...(poolOptions.home ? { home: poolOptions.home } : {}), poolId: plan.pool ?? null, tenantHint });
|
|
280
285
|
} catch (error) {
|
|
281
|
-
|
|
286
|
+
const poolIds = Array.isArray(error?.poolIds) ? error.poolIds : [];
|
|
287
|
+
const code = error?.code === "pool_ambiguous" ? "pool_ambiguous" : "pool_not_installed";
|
|
288
|
+
return refuse(io, code, 3, { detail: String(error?.message ?? error).slice(0, 300) }, {
|
|
289
|
+
...vars,
|
|
290
|
+
tenant: tenantHint ?? undefined,
|
|
291
|
+
poolIds,
|
|
292
|
+
poolId: plan.pool ?? undefined,
|
|
293
|
+
retryWithPool: poolIds.length ? `${authLine} --pool ${poolIds[0]}` : undefined,
|
|
294
|
+
});
|
|
282
295
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
const { findKnownCheckouts } = await import("../staging-pool/checkouts.mjs");
|
|
286
|
-
const [candidate] = await findKnownCheckouts({ ...(poolOptions.home ? { home: poolOptions.home } : {}), ...(poolOptions.isStableCheckout ? { isStable: poolOptions.isStableCheckout } : {}) }).catch(() => []);
|
|
287
|
-
return refuse(io, "pool_not_configured", 3, {}, { ...vars, checkout: candidate?.path });
|
|
288
|
-
}
|
|
289
|
-
Object.assign(vars, { allowed: Object.keys(found.binding.personas ?? {}) });
|
|
296
|
+
const found = { binding: ctxPool.poolBinding };
|
|
297
|
+
Object.assign(vars, { allowed: Object.keys(found.binding.personas ?? {}), tenant: ctxPool.tenant ?? undefined, poolId: ctxPool.poolId ?? undefined });
|
|
290
298
|
const { findPersona } = await loadBinding();
|
|
291
299
|
const persona = findPersona(found.binding, plan.persona);
|
|
292
300
|
if (!persona) return refuse(io, "persona_not_allowlisted", 2, {}, vars);
|
|
@@ -303,7 +311,7 @@ export async function runStagingAuth({ plan, host, env, deps, socketRun, resolve
|
|
|
303
311
|
dropGone(ctx, generation);
|
|
304
312
|
let claim;
|
|
305
313
|
try {
|
|
306
|
-
claim = await poolModule.takeForLane({ persona: persona.email, owner: `lane-${plan.lane}@${host}`,
|
|
314
|
+
claim = await poolModule.takeForLane({ persona: persona.email, owner: `lane-${plan.lane}@${host}`, poolId: ctxPool.poolId, tenantHint, ...poolOptions });
|
|
307
315
|
} catch (error) {
|
|
308
316
|
if (error?.code === "pool_empty") {
|
|
309
317
|
return refuse(io, "pool_empty", 3, Object.fromEntries(["reason", "detail", ...GUIDED_FIELDS].filter((key) => error[key] !== undefined).map((key) => [key, error[key]])), vars);
|
|
@@ -1,14 +1,10 @@
|
|
|
1
|
-
// BOT-2084: the
|
|
1
|
+
// BOT-2084 / BOT-2648: the staging session pool binding.
|
|
2
2
|
//
|
|
3
|
-
// A
|
|
4
|
-
//
|
|
5
|
-
// validates
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
import { readFile } from "node:fs/promises";
|
|
9
|
-
import { dirname, join, parse } from "node:path";
|
|
10
|
-
|
|
11
|
-
export const AGENT_BINDING_FILE = ".botbuddy-agent.json";
|
|
3
|
+
// A tenant registers its binding in the BotBuddy control plane
|
|
4
|
+
// (register_staging_pool); `bb staging-pool install --tenant` reads it and
|
|
5
|
+
// this module parses and validates it into the camelCase shape the pool uses. It
|
|
6
|
+
// is never read from a repository checkout. Node-stdlib-only and self-contained:
|
|
7
|
+
// the installed job keeps a validated snapshot in its config.
|
|
12
8
|
|
|
13
9
|
export class BindingError extends Error {
|
|
14
10
|
constructor(message, code = "pool_binding_invalid") {
|
|
@@ -29,8 +25,21 @@ const PUBLIC_KEY_RE = /^(sb_publishable_[A-Za-z0-9_-]{16,128}|eyJ[A-Za-z0-9_-]+\
|
|
|
29
25
|
export const ADAPTERS = Object.freeze(["supabase-gotrue-totp"]);
|
|
30
26
|
export const MAX_POOL_SIZE = 4;
|
|
31
27
|
|
|
28
|
+
// A legacy Supabase key is a JWT; only the public `anon` role is a publishable key
|
|
29
|
+
// (Codex P1, #1323: a service_role JWT is a secret and is never installed).
|
|
30
|
+
export function isPublicAnonJwt(value) {
|
|
31
|
+
const parts = String(value).split(".");
|
|
32
|
+
if (parts.length !== 3) return false;
|
|
33
|
+
try {
|
|
34
|
+
const claims = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
|
|
35
|
+
return claims !== null && typeof claims === "object" && claims.role === "anon";
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
32
41
|
function need(condition, message) {
|
|
33
|
-
if (!condition) throw new BindingError(
|
|
42
|
+
if (!condition) throw new BindingError(`staging_session_pool binding: ${message}`);
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
// raw: the parsed .botbuddy-agent.json object. Returns null when the repo does not
|
|
@@ -48,12 +57,15 @@ export function parsePoolBinding(raw) {
|
|
|
48
57
|
need(typeof section.app_origin === "string", "app_origin is required");
|
|
49
58
|
need(typeof section.supabase_url === "string", "supabase_url is required");
|
|
50
59
|
need(typeof section.publishable_key === "string" && PUBLIC_KEY_RE.test(section.publishable_key), "publishable_key must be the public publishable/anon key");
|
|
60
|
+
need(!section.publishable_key.startsWith("eyJ") || isPublicAnonJwt(section.publishable_key), "a legacy JWT publishable_key must carry role anon (a service_role key is a secret)");
|
|
51
61
|
need(typeof section.storage_key === "string" && STORAGE_KEY_RE.test(section.storage_key), "storage_key must be the app's supabase-js auth storage key");
|
|
52
62
|
need(Array.isArray(section.production_origins ?? []), "production_origins must be an array");
|
|
53
63
|
need(typeof section.password_env_file === "string" && /^(~\/|\/)/.test(section.password_env_file), "password_env_file must be an absolute or ~/ path");
|
|
54
64
|
need(typeof section.launchd_label === "string" && LABEL_RE.test(section.launchd_label), "launchd_label must be a reverse-DNS label");
|
|
55
|
-
|
|
56
|
-
|
|
65
|
+
// BOT-2648: docs is optional, and install_command is retired (install reads the
|
|
66
|
+
// control plane by tenant); a pre-registry snapshot may still carry it.
|
|
67
|
+
need(section.docs === undefined || (typeof section.docs === "string" && section.docs.length > 0), "docs must be a non-empty string when present");
|
|
68
|
+
need(section.install_command === undefined || typeof section.install_command === "string", "install_command must be a string when present");
|
|
57
69
|
const personas = section.personas;
|
|
58
70
|
need(personas && typeof personas === "object" && Object.keys(personas).length > 0, "personas must name at least one persona");
|
|
59
71
|
const parsedPersonas = {};
|
|
@@ -84,7 +96,6 @@ export function parsePoolBinding(raw) {
|
|
|
84
96
|
personas: Object.freeze(parsedPersonas),
|
|
85
97
|
launchdLabel: section.launchd_label,
|
|
86
98
|
docs: section.docs,
|
|
87
|
-
installCommand: section.install_command,
|
|
88
99
|
});
|
|
89
100
|
}
|
|
90
101
|
|
|
@@ -94,29 +105,3 @@ export function findPersona(binding, aliasOrEmail) {
|
|
|
94
105
|
if (Object.hasOwn(binding.personas, key)) return binding.personas[key];
|
|
95
106
|
return Object.values(binding.personas).find((persona) => persona.email === key) ?? null;
|
|
96
107
|
}
|
|
97
|
-
|
|
98
|
-
// Walk up from cwd to the committed binding file; null when none or no section.
|
|
99
|
-
export async function readRepoPoolBinding(cwd = process.cwd(), { read = readFile } = {}) {
|
|
100
|
-
let dir = cwd;
|
|
101
|
-
const root = parse(dir).root;
|
|
102
|
-
while (true) {
|
|
103
|
-
let text = null;
|
|
104
|
-
try {
|
|
105
|
-
text = await read(join(dir, AGENT_BINDING_FILE), "utf8");
|
|
106
|
-
} catch (error) {
|
|
107
|
-
if (error?.code !== "ENOENT") throw error;
|
|
108
|
-
}
|
|
109
|
-
if (text !== null) {
|
|
110
|
-
let raw;
|
|
111
|
-
try {
|
|
112
|
-
raw = JSON.parse(text);
|
|
113
|
-
} catch {
|
|
114
|
-
throw new BindingError(`${AGENT_BINDING_FILE} is not valid JSON`);
|
|
115
|
-
}
|
|
116
|
-
const binding = parsePoolBinding(raw);
|
|
117
|
-
return binding ? { binding, repoRoot: dir } : null;
|
|
118
|
-
}
|
|
119
|
-
if (dir === root) return null;
|
|
120
|
-
dir = dirname(dir);
|
|
121
|
-
}
|
|
122
|
-
}
|