@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.
@@ -1,7 +1,7 @@
1
1
  {
2
- "release_version": "1.60.7",
3
- "source_commit": "6299d1f0f325871a33f5aa28bff8dc8029e7350c",
4
- "source_sha256": "ac9f92aeaec3cf0cb18de06f1e16b0ee2c44c413ec7c84866f949909611098f1",
2
+ "release_version": "1.60.8",
3
+ "source_commit": "057670de6f7ac5dc8e8e673dab13a317aef876e9",
4
+ "source_sha256": "3678a4ff49dda524b2b97e5a9d57249b64041f587da4db0c936b2f650647c6ec",
5
5
  "target": "darwin-arm64",
6
- "sha256": "1e24ee9c3fa9ac6447ff9f0c8096d80c4e2a98348d0e3b0277d406dfe8109e17"
6
+ "sha256": "234246b7e1b8634420dbe2bceba35a4efa51a0ade206b09882f9c80a0aa66bfb"
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
- "release_version": "1.60.7",
3
- "source_commit": "6299d1f0f325871a33f5aa28bff8dc8029e7350c",
4
- "source_sha256": "ac9f92aeaec3cf0cb18de06f1e16b0ee2c44c413ec7c84866f949909611098f1",
2
+ "release_version": "1.60.8",
3
+ "source_commit": "057670de6f7ac5dc8e8e673dab13a317aef876e9",
4
+ "source_sha256": "3678a4ff49dda524b2b97e5a9d57249b64041f587da4db0c936b2f650647c6ec",
5
5
  "target": "linux-x64",
6
- "sha256": "68dfb56ddf1b4647dcd00644d88489fe2022b4136d72443e33f83a86ecb270bb"
6
+ "sha256": "c29069dc84ea8273ea4b560816eb130a82dfbba083a6d8149edecd8cc85b534c"
7
7
  }
@@ -1,21 +1,21 @@
1
1
  {
2
2
  "schema_version": 1,
3
- "release_version": "1.60.7",
4
- "source_commit": "6299d1f0f325871a33f5aa28bff8dc8029e7350c",
5
- "source_sha256": "ac9f92aeaec3cf0cb18de06f1e16b0ee2c44c413ec7c84866f949909611098f1",
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": "1e24ee9c3fa9ac6447ff9f0c8096d80c4e2a98348d0e3b0277d406dfe8109e17",
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": "68dfb56ddf1b4647dcd00644d88489fe2022b4136d72443e33f83a86ecb270bb",
18
+ "sha256": "c29069dc84ea8273ea4b560816eb130a82dfbba083a6d8149edecd8cc85b534c",
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.7",
3
+ "version": "1.60.8",
4
4
  "description": "BotBuddy \u2014 Swarm coordination CLI for multi-agent workflows",
5
5
  "type": "module",
6
6
  "bin": {
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
- deps = { ...deps, sessionToken: credential.token, sessionId: deps.sessionId ?? credential.sessionId ?? null, credentialAgentId: credential.agentId ?? null };
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
  }
@@ -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 vars = { lane: plan.lane, persona: plan.persona, cwd, ticketFlag: ticketFlagFor(cwd) };
277
- let found;
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
- found = await (await loadBinding()).readRepoPoolBinding(cwd);
284
+ ctxPool = poolModule.resolveInstalledPool({ ...(poolOptions.home ? { home: poolOptions.home } : {}), poolId: plan.pool ?? null, tenantHint });
280
285
  } catch (error) {
281
- return refuse(io, error?.code === "pool_binding_invalid" ? "pool_binding_invalid" : "pool_not_configured", 3, { detail: String(error?.message ?? error) }, vars);
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
- if (!found) {
284
- // Name a stable checkout that declares a pool, if this Mac has one on record.
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}`, cwd, ...poolOptions });
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 per-repo staging session pool binding.
1
+ // BOT-2084 / BOT-2648: the staging session pool binding.
2
2
  //
3
- // A repository opts in by declaring `staging_session_pool` in its committed
4
- // `.botbuddy-agent.json` (next to its tenant binding). This module parses and
5
- // validates that section into the camelCase shape the pool uses. Node-stdlib-only
6
- // and self-contained: the installed job keeps a validated snapshot in its config.
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(`${AGENT_BINDING_FILE} staging_session_pool: ${message}`);
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
- need(typeof section.docs === "string" && section.docs.length > 0, "docs (where the pool is documented) is required");
56
- need(typeof section.install_command === "string" && section.install_command.length > 0, "install_command (the human's install line) is required");
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
- }