@botbuddy/cli 1.64.0 → 1.64.2
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/staging-auth.mjs +41 -1
- package/src/staging-pool/cli.mjs +1 -0
- package/src/staging-pool/config.mjs +8 -2
- package/src/staging-pool/errors.mjs +7 -3
- package/src/staging-pool/refresh-runtime.mjs +1 -1
- package/src/wait-core.mjs +14 -0
- package/src/wait.mjs +6 -0
|
Binary file
|
|
Binary file
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"release_version": "1.64.
|
|
3
|
-
"source_commit": "
|
|
4
|
-
"source_sha256": "
|
|
2
|
+
"release_version": "1.64.2",
|
|
3
|
+
"source_commit": "48e7d1b3e42d27353ef734e0c61402b9051eb51c",
|
|
4
|
+
"source_sha256": "8f806cc487b47875fd051cf2c3f4cfbcf4a3b3e7f25930cd81ff613a2e400d86",
|
|
5
5
|
"target": "darwin-arm64",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "1f1bff159d7c87ec19a13539afee99b58a325bb695d1ab2c22c0c794d59907d6"
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"release_version": "1.64.
|
|
3
|
-
"source_commit": "
|
|
4
|
-
"source_sha256": "
|
|
2
|
+
"release_version": "1.64.2",
|
|
3
|
+
"source_commit": "48e7d1b3e42d27353ef734e0c61402b9051eb51c",
|
|
4
|
+
"source_sha256": "8f806cc487b47875fd051cf2c3f4cfbcf4a3b3e7f25930cd81ff613a2e400d86",
|
|
5
5
|
"target": "linux-x64",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "806aa3ac12cba1b8fce8e29848f1c47be489201e7dd1f3347f0b9ce0b7ec2c1a"
|
|
7
7
|
}
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": 1,
|
|
3
|
-
"release_version": "1.64.
|
|
4
|
-
"source_commit": "
|
|
5
|
-
"source_sha256": "
|
|
3
|
+
"release_version": "1.64.2",
|
|
4
|
+
"source_commit": "48e7d1b3e42d27353ef734e0c61402b9051eb51c",
|
|
5
|
+
"source_sha256": "8f806cc487b47875fd051cf2c3f4cfbcf4a3b3e7f25930cd81ff613a2e400d86",
|
|
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": "1f1bff159d7c87ec19a13539afee99b58a325bb695d1ab2c22c0c794d59907d6",
|
|
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": "806aa3ac12cba1b8fce8e29848f1c47be489201e7dd1f3347f0b9ce0b7ec2c1a",
|
|
19
19
|
"mode": "0755"
|
|
20
20
|
}
|
|
21
21
|
}
|
package/package.json
CHANGED
package/src/pw/staging-auth.mjs
CHANGED
|
@@ -263,6 +263,35 @@ export function refuse({ stdout, stderr }, code, exitCode = 3, extra = {}, vars
|
|
|
263
263
|
async function loadPool() { return import("../staging-pool/cli.mjs"); }
|
|
264
264
|
async function loadBinding() { return import("../staging-pool/binding.mjs"); }
|
|
265
265
|
|
|
266
|
+
// BOT-2894: pool_empty is an agent-visible symptom, but recovery is owned by
|
|
267
|
+
// the control plane. This fixed call sends only the registered tenant/pool and
|
|
268
|
+
// persona; no host, path, command, credential or retry policy can cross it.
|
|
269
|
+
async function requestPoolRefill({ ctxPool, persona, deps, env }) {
|
|
270
|
+
try {
|
|
271
|
+
if (deps.requestPoolRefill) return await deps.requestPoolRefill({
|
|
272
|
+
tenant: ctxPool.tenant, poolId: ctxPool.poolId, persona,
|
|
273
|
+
});
|
|
274
|
+
const bin = ctxPool.installed?.botbuddyBin;
|
|
275
|
+
if (!bin || !ctxPool.tenant) return { ok: false, code: "POOL_REFILL_UNAVAILABLE" };
|
|
276
|
+
const run = deps.poolRefillExec ?? execFileSync;
|
|
277
|
+
const stdout = String(run(bin, ["call", "request_staging_pool_refill", "--pin", ctxPool.tenant, "--json", JSON.stringify({ pool_id: ctxPool.poolId, persona })], {
|
|
278
|
+
encoding: "utf8", timeout: 10_000, env, stdio: ["ignore", "pipe", "ignore"],
|
|
279
|
+
}));
|
|
280
|
+
const parsed = JSON.parse(stdout);
|
|
281
|
+
return {
|
|
282
|
+
ok: parsed?.ok === true,
|
|
283
|
+
...(typeof parsed?.job_id === "string" ? { job_id: parsed.job_id } : {}),
|
|
284
|
+
...(typeof parsed?.status === "string" ? { status: parsed.status } : {}),
|
|
285
|
+
...(typeof parsed?.code === "string" ? { code: parsed.code } : {}),
|
|
286
|
+
};
|
|
287
|
+
} catch {
|
|
288
|
+
// Recovery request failure does not turn an already truthful pool_empty
|
|
289
|
+
// into a hidden success. The one wait/retry remains bounded and reports its
|
|
290
|
+
// original symptom; the next attempt will request recovery again.
|
|
291
|
+
return { ok: false, code: "POOL_REFILL_UNAVAILABLE" };
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
266
295
|
/**
|
|
267
296
|
* `auth --persona <alias>` after the wrapper has proven: environment=staging, a
|
|
268
297
|
* lock-gated lane held by this exact agent, and a staging (or untracked) browser
|
|
@@ -316,13 +345,24 @@ export async function runStagingAuth({ plan, host, env, deps, socketRun, resolve
|
|
|
316
345
|
claim = await poolModule.takeForLane({ persona: persona.email, owner: `lane-${plan.lane}@${host}`, poolId: ctxPool.poolId, tenantHint, ...poolOptions });
|
|
317
346
|
} catch (error) {
|
|
318
347
|
if (error?.code === "pool_empty") {
|
|
348
|
+
const refill = await requestPoolRefill({ ctxPool, persona: persona.email, deps, env });
|
|
319
349
|
// The refusal's code is the pool's health reason when it is unhealthy (e.g. a
|
|
320
350
|
// count-0 persona is persona_not_configured), so code, message and help agree.
|
|
321
351
|
const reasonCode = typeof error.reason === "string" && error.reason !== "healthy" && isKnownCode(error.reason) ? error.reason : "pool_empty";
|
|
352
|
+
// A terminal refill receipt is a real, durable recovery boundary. Without
|
|
353
|
+
// one, never tell the agent to await a signal that does not exist.
|
|
354
|
+
const refillNext = refill.ok === true && typeof refill.job_id === "string"
|
|
355
|
+
? `bb wait 'staging-pool-refill:id=${refill.job_id}' && ${vars.retry ?? authLine} # once; if it fails, report this code`
|
|
356
|
+
: undefined;
|
|
322
357
|
// BOT-2836: a healthy empty pool is the agent's own wait-and-retry, so it is
|
|
323
358
|
// rendered here with this lane; an unhealthy pool keeps the pool's health guidance.
|
|
324
359
|
const forwarded = reasonCode === "pool_empty" ? ["reason", "detail"] : ["reason", "detail", ...GUIDED_FIELDS];
|
|
325
|
-
|
|
360
|
+
const code = reasonCode === "pool_empty" && !refillNext ? "pool_refill_unavailable" : reasonCode;
|
|
361
|
+
return refuse(io, code, 3, {
|
|
362
|
+
...Object.fromEntries(forwarded.filter((key) => error[key] !== undefined).map((key) => [key, error[key]])),
|
|
363
|
+
...(refillNext ? { next: refillNext } : {}),
|
|
364
|
+
automatic_refill: refill.ok === true ? { requested: true, ...(refill.job_id ? { job_id: refill.job_id } : {}), ...(refill.status ? { status: refill.status } : {}) } : { requested: false, code: refill.code },
|
|
365
|
+
}, vars);
|
|
326
366
|
}
|
|
327
367
|
const code = typeof error?.code === "string" && isKnownCode(error.code) ? error.code : "pool_take_failed";
|
|
328
368
|
return refuse(io, code, 3, { detail: String(error?.message ?? error).slice(0, 300) }, vars);
|
package/src/staging-pool/cli.mjs
CHANGED
|
@@ -514,6 +514,7 @@ export async function runStagingPool(argv, deps = {}) {
|
|
|
514
514
|
rawBinding,
|
|
515
515
|
poolBinding,
|
|
516
516
|
botbuddyBin,
|
|
517
|
+
nodeBin,
|
|
517
518
|
intervalSeconds: explicitInterval ?? existing?.intervalSeconds ?? DEFAULT_INTERVAL_SECONDS,
|
|
518
519
|
existingPersonas: existing?.personas,
|
|
519
520
|
tenant,
|
|
@@ -37,7 +37,7 @@ export function poolHomePaths({ home = homedir(), poolId, root } = {}) {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
// The installed config: { version, binding: <raw .botbuddy-agent.json subset>,
|
|
40
|
-
// personas: { email: count }, botbuddyBin, intervalSeconds }. Returns it with the
|
|
40
|
+
// personas: { email: count }, botbuddyBin, nodeBin, intervalSeconds }. Returns it with the
|
|
41
41
|
// parsed binding attached as `poolBinding`.
|
|
42
42
|
export function validateInstalledConfig(config) {
|
|
43
43
|
if (!config || typeof config !== "object") throw new ConfigError("Pool config is not an object.", "config_invalid");
|
|
@@ -52,6 +52,11 @@ export function validateInstalledConfig(config) {
|
|
|
52
52
|
if (typeof config.botbuddyBin !== "string" || !config.botbuddyBin.startsWith("/")) {
|
|
53
53
|
throw new ConfigError("Pool config botbuddyBin must be an absolute path (re-run install).", "config_invalid");
|
|
54
54
|
}
|
|
55
|
+
// `nodeBin` is added by BOT-2894. Older human-installed pools remain valid,
|
|
56
|
+
// but an autonomous Helper may only advertise a pool that pins its runtime.
|
|
57
|
+
if (config.nodeBin !== undefined && (typeof config.nodeBin !== "string" || !config.nodeBin.startsWith("/"))) {
|
|
58
|
+
throw new ConfigError("Pool config nodeBin must be an absolute path (re-run install).", "config_invalid");
|
|
59
|
+
}
|
|
55
60
|
if (!Number.isInteger(config.intervalSeconds) || config.intervalSeconds < 60 || config.intervalSeconds > 3600) {
|
|
56
61
|
throw new ConfigError("Pool config intervalSeconds must be an integer 60-3600.", "config_invalid");
|
|
57
62
|
}
|
|
@@ -87,7 +92,7 @@ export function readInstalledConfig(path, { readFile = readFileSync } = {}) {
|
|
|
87
92
|
}
|
|
88
93
|
|
|
89
94
|
// rawBinding: { tenant, staging_session_pool } exactly as committed.
|
|
90
|
-
export function buildInstalledConfig({ rawBinding, poolBinding, botbuddyBin, intervalSeconds = DEFAULT_INTERVAL_SECONDS, existingPersonas, tenant, registry, installedAt }) {
|
|
95
|
+
export function buildInstalledConfig({ rawBinding, poolBinding, botbuddyBin, nodeBin, intervalSeconds = DEFAULT_INTERVAL_SECONDS, existingPersonas, tenant, registry, installedAt }) {
|
|
91
96
|
const personas = {};
|
|
92
97
|
for (const persona of Object.values(poolBinding.personas)) {
|
|
93
98
|
personas[persona.email] = Number.isInteger(existingPersonas?.[persona.email]) ? existingPersonas[persona.email] : persona.defaultCount;
|
|
@@ -97,6 +102,7 @@ export function buildInstalledConfig({ rawBinding, poolBinding, botbuddyBin, int
|
|
|
97
102
|
binding: { tenant: rawBinding.tenant, staging_session_pool: rawBinding.staging_session_pool },
|
|
98
103
|
personas,
|
|
99
104
|
botbuddyBin,
|
|
105
|
+
...(nodeBin ? { nodeBin } : {}),
|
|
100
106
|
intervalSeconds,
|
|
101
107
|
// BOT-2648: installed from the tenant's control-plane registration.
|
|
102
108
|
...(tenant ? { tenant, poolId: poolBinding.poolId, source: "control-plane" } : {}),
|
|
@@ -135,8 +135,12 @@ export const ERROR_CODES = Object.freeze({
|
|
|
135
135
|
// already asked the human-installed job to refill now, so the agent waits once and retries.
|
|
136
136
|
pool_empty: entry("agent", "take",
|
|
137
137
|
"The staging session pool is healthy but has no ready session for {persona}.",
|
|
138
|
-
"Agents took every ready session since the last tick, or the ready ones were dead or AAL1 (factor rotated) and were discarded;
|
|
139
|
-
"bb wait
|
|
138
|
+
"Agents took every ready session since the last tick, or the ready ones were dead or AAL1 (factor rotated) and were discarded; automatic refill creates a durable job before the single retry.",
|
|
139
|
+
"Automatic refill is requested by bb pw auth; wait for its refill job, then {retry} once; if it fails, report this code", "retry_once"),
|
|
140
|
+
pool_refill_unavailable: entry("agent", "take",
|
|
141
|
+
"The staging session pool is empty and automatic refill did not return a durable job.",
|
|
142
|
+
"The control plane was unavailable or rejected the refill request, so there is no job signal to await.",
|
|
143
|
+
"{retry} # automatic refill did not return a job; retry once; if it repeats, report this code", "retry_once"),
|
|
140
144
|
job_not_loaded: entry("human", "install",
|
|
141
145
|
"The staging session pool is not installed (or not loaded) on this Mac.",
|
|
142
146
|
"The one-time install has not been run here, or the Mac was migrated.",
|
|
@@ -322,7 +326,7 @@ export const AUTH_ONLY_CODES = Object.freeze(new Set([
|
|
|
322
326
|
"tenant_binding_required", "persona_not_allowlisted", "lane_browser_not_open", "browser_session_not_staging", "persistent_profile_refused",
|
|
323
327
|
"session_install_failed", "staging_session_cleanup_pending", "session_not_aal2", "session_expired",
|
|
324
328
|
"session_persona_mismatch", "slot_state_invalid", "slot_state_unreadable", "daemon_node_unsupported",
|
|
325
|
-
"daemon_run_failed", "daemon_result_missing", "daemon_result_invalid", "pool_take_failed", "pool_empty",
|
|
329
|
+
"daemon_run_failed", "daemon_result_missing", "daemon_result_invalid", "pool_take_failed", "pool_empty", "pool_refill_unavailable",
|
|
326
330
|
]));
|
|
327
331
|
|
|
328
332
|
// `bb staging-pool` subcommands only a human runs. A refusal from one names
|
|
@@ -91,7 +91,7 @@ export async function refreshExistingRuntime({ options, home, uid, env, exec, bu
|
|
|
91
91
|
const contentDigest = hash.digest("hex");
|
|
92
92
|
const runtimeName = `runtime-${metadata.version}-${contentDigest}`;
|
|
93
93
|
const runtime = join(root, runtimeName);
|
|
94
|
-
const config = validateInstalledConfig({ ...installed, binding: rawBinding, personas, source: "control-plane", tenant, poolId, registry: { commit_sha: row.commit_sha ?? null, updated_at: row.updated_at ?? null }, runtime: { version: metadata.version, sha256: contentDigest } });
|
|
94
|
+
const config = validateInstalledConfig({ ...installed, binding: rawBinding, personas, nodeBin: installed.nodeBin ?? await fs.realpath(process.execPath), source: "control-plane", tenant, poolId, registry: { commit_sha: row.commit_sha ?? null, updated_at: row.updated_at ?? null }, runtime: { version: metadata.version, sha256: contentDigest } });
|
|
95
95
|
const token = randomUUID();
|
|
96
96
|
const lockPath = join(root, ".runtime-refresh.lock");
|
|
97
97
|
const lock = await fs.open(lockPath, "wx", 0o600);
|
package/src/wait-core.mjs
CHANGED
|
@@ -216,6 +216,15 @@ const VALIDATORS = {
|
|
|
216
216
|
}
|
|
217
217
|
return { runId };
|
|
218
218
|
},
|
|
219
|
+
"staging-pool-refill": (p, raw) => {
|
|
220
|
+
const pair = /^\s*id\s*=\s*([0-9A-Fa-f-]+)\s*$/.exec(raw ?? "");
|
|
221
|
+
if (!pair || Object.keys(p).length !== 1 || p.id !== pair[1]) {
|
|
222
|
+
throw new Error("staging-pool-refill needs exactly one id=<refill job UUID>");
|
|
223
|
+
}
|
|
224
|
+
const jobId = pair[1].toLowerCase();
|
|
225
|
+
if (!CANONICAL_UUID_RE.test(jobId)) throw new Error("staging-pool-refill id must be a canonical UUID");
|
|
226
|
+
return { jobId };
|
|
227
|
+
},
|
|
219
228
|
// BOT-2629: this is deliberately a dedicated condition rather than a raw
|
|
220
229
|
// event escape hatch. The UUID is chosen before dispatch and the exact subject
|
|
221
230
|
// prevents another run on the same idle/re-adopted lease waking this waiter.
|
|
@@ -1295,6 +1304,11 @@ function conditionMatchesSignal(condition, signal, waitSessionId) {
|
|
|
1295
1304
|
signal.subject_key === `run:${params.runId}` &&
|
|
1296
1305
|
signal.payload?.run_id === params.runId &&
|
|
1297
1306
|
COMMAND_RUN_TERMINAL_STATUSES.has(signal.payload?.status);
|
|
1307
|
+
case "staging-pool-refill":
|
|
1308
|
+
return signal.signal_type === "staging_pool_refill" &&
|
|
1309
|
+
signal.subject_key === `staging-pool-refill:${params.jobId}` &&
|
|
1310
|
+
signal.payload?.job_id === params.jobId &&
|
|
1311
|
+
["completed", "failed"].includes(signal.payload?.status);
|
|
1298
1312
|
case "stack-run":
|
|
1299
1313
|
return signal.signal_type === "stack_run" && signal.subject_key === `stack-run:${params.id}` &&
|
|
1300
1314
|
signal.payload?.run_id === params.id && Number.isInteger(signal.payload?.exit_code);
|
package/src/wait.mjs
CHANGED
|
@@ -165,6 +165,10 @@ CONDITIONS (TYPE:key=val,key=val — repeat for several; --any wakes on the fir
|
|
|
165
165
|
condition, and keep this wait's result pending: its
|
|
166
166
|
return is what resumes your task. A run whose worker is
|
|
167
167
|
lost ends owner_lost (bb run inspect <run_id>).
|
|
168
|
+
staging-pool-refill:id=<job UUID> BOT-2894: a requested staging-pool refill reaching
|
|
169
|
+
completed or failed. This is the only condition of its
|
|
170
|
+
wait and is available to each tenant agent that requested
|
|
171
|
+
the deduped job; its terminal receipt is safe to display.
|
|
168
172
|
stack-run:id=<run UUID> one named bb stack run reaching its terminal outcome
|
|
169
173
|
(BOT-2629). Choose the UUID BEFORE dispatch and pass it as
|
|
170
174
|
bb stack run --run-id <UUID>; the receipt carries exit_code,
|
|
@@ -648,6 +652,8 @@ async function registerWait(opts, conditions, deadlineIso) {
|
|
|
648
652
|
// back to an untracked wait would only hide that error behind a timeout.
|
|
649
653
|
"command_run_unavailable",
|
|
650
654
|
"command_run_single_condition_required",
|
|
655
|
+
"staging_pool_refill_unavailable",
|
|
656
|
+
"staging_pool_refill_single_condition_required",
|
|
651
657
|
]);
|
|
652
658
|
if (INVALID_CONDITION_CODES.has(body.error)) {
|
|
653
659
|
const err = new Error(body.detail || body.error);
|