@botbuddy/cli 1.64.11 → 1.64.12
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/staging-pool/cli.mjs +3 -0
- package/src/staging-pool/errors.mjs +1 -1
- package/src/staging-pool/pool.mjs +39 -5
- package/src/staging-pool/signin-gotrue-totp.mjs +16 -5
|
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.12",
|
|
3
|
+
"source_commit": "ac15a977c04652b558eb4353f232ba4e26c87740",
|
|
4
|
+
"source_sha256": "6eadb8a389e466c1de0b4e3ad98a4978482eef4ae4afa59013f367efdd9b8805",
|
|
5
5
|
"target": "darwin-arm64",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "50423b55ca6646d05cc79d73f3eac4ecc55f0d7bbed05f288e8b297c2a5b3cec"
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"release_version": "1.64.
|
|
3
|
-
"source_commit": "
|
|
4
|
-
"source_sha256": "
|
|
2
|
+
"release_version": "1.64.12",
|
|
3
|
+
"source_commit": "ac15a977c04652b558eb4353f232ba4e26c87740",
|
|
4
|
+
"source_sha256": "6eadb8a389e466c1de0b4e3ad98a4978482eef4ae4afa59013f367efdd9b8805",
|
|
5
5
|
"target": "linux-x64",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "851f72b5be98d48eb631ea5dac259e0be32bcc1cf218c53e666bcdcc0bd02e6d"
|
|
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.12",
|
|
4
|
+
"source_commit": "ac15a977c04652b558eb4353f232ba4e26c87740",
|
|
5
|
+
"source_sha256": "6eadb8a389e466c1de0b4e3ad98a4978482eef4ae4afa59013f367efdd9b8805",
|
|
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": "50423b55ca6646d05cc79d73f3eac4ecc55f0d7bbed05f288e8b297c2a5b3cec",
|
|
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": "851f72b5be98d48eb631ea5dac259e0be32bcc1cf218c53e666bcdcc0bd02e6d",
|
|
19
19
|
"mode": "0755"
|
|
20
20
|
}
|
|
21
21
|
}
|
package/package.json
CHANGED
package/src/staging-pool/cli.mjs
CHANGED
|
@@ -284,7 +284,10 @@ export async function takeForLane({
|
|
|
284
284
|
const g = guidance("pool_empty", guidanceVars(ctx, { persona: entry.alias }));
|
|
285
285
|
effective = { ...health, reason: "pool_empty", agentAction: g.agent_action, remedy: remedyLine(g), message: g.message, cause: g.cause, actor: g.actor_label, next: g.next, doc: g.doc, help: g.help };
|
|
286
286
|
}
|
|
287
|
+
// BOT-2952: the pool's own detail (an Auth outage at take time) survives the health's.
|
|
288
|
+
const poolDetail = error.detail;
|
|
287
289
|
for (const field of GUIDANCE_FIELDS) if (effective[field] !== undefined) error[field] = effective[field];
|
|
290
|
+
if (poolDetail) error.detail = [poolDetail, effective.detail].filter(Boolean).join(" ");
|
|
288
291
|
throw error;
|
|
289
292
|
}
|
|
290
293
|
}
|
|
@@ -135,7 +135,7 @@ 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; automatic refill creates a durable job before the single retry.",
|
|
138
|
+
"Agents took every ready session since the last tick, or the ready ones were dead or AAL1 (factor rotated) and were discarded, or staging Auth did not answer their liveness check and they were kept (detail says so); automatic refill creates a durable job before the single retry.",
|
|
139
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
140
|
pool_refill_unavailable: entry("agent", "take",
|
|
141
141
|
"The staging session pool is empty and automatic refill did not return a durable job.",
|
|
@@ -483,6 +483,7 @@ export function createSessionPool({
|
|
|
483
483
|
const entry = (health.personas[persona] ??= {
|
|
484
484
|
belowTargetTicks: 0,
|
|
485
485
|
consecutiveMintFailures: 0,
|
|
486
|
+
backoffMintFailures: 0,
|
|
486
487
|
nextMintAttemptAt: null,
|
|
487
488
|
lastMintError: null,
|
|
488
489
|
});
|
|
@@ -505,20 +506,33 @@ export function createSessionPool({
|
|
|
505
506
|
await mintIntoReady(persona);
|
|
506
507
|
counts[persona] += 1;
|
|
507
508
|
entry.consecutiveMintFailures = 0;
|
|
509
|
+
entry.backoffMintFailures = 0;
|
|
508
510
|
entry.nextMintAttemptAt = null;
|
|
509
511
|
entry.lastMintError = null;
|
|
510
512
|
} catch (error) {
|
|
511
513
|
summary.mintFailures += 1;
|
|
512
514
|
entry.consecutiveMintFailures += 1;
|
|
513
515
|
entry.lastMintError = error?.code ?? "error";
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
516
|
+
// BOT-2952: an outage that never got a password grant created no AAL1 session and
|
|
517
|
+
// ran no MFA verify, so it retries on the next tick and does not lengthen a later
|
|
518
|
+
// backoff. A 429 is Auth asking us to slow down, so it backs off like any other
|
|
519
|
+
// failure, on its own streak.
|
|
520
|
+
const outage =
|
|
521
|
+
error?.transient === true && error?.passwordGrantSucceeded === false && error?.rateLimited !== true;
|
|
522
|
+
if (outage) {
|
|
523
|
+
entry.nextMintAttemptAt = null;
|
|
524
|
+
} else {
|
|
525
|
+
entry.backoffMintFailures = (entry.backoffMintFailures ?? 0) + 1;
|
|
526
|
+
entry.nextMintAttemptAt = new Date(
|
|
527
|
+
now() + nextMintBackoffMs({ failures: entry.backoffMintFailures, intervalMs }),
|
|
528
|
+
).toISOString();
|
|
529
|
+
}
|
|
517
530
|
logger({
|
|
518
531
|
event: "mint_failed",
|
|
519
532
|
persona,
|
|
520
533
|
code: entry.lastMintError,
|
|
521
534
|
message: error?.message ?? String(error),
|
|
535
|
+
outage,
|
|
522
536
|
consecutiveMintFailures: entry.consecutiveMintFailures,
|
|
523
537
|
nextMintAttemptAt: entry.nextMintAttemptAt,
|
|
524
538
|
});
|
|
@@ -567,6 +581,7 @@ export function createSessionPool({
|
|
|
567
581
|
}
|
|
568
582
|
}
|
|
569
583
|
candidates.sort((a, b) => b.verifiedAt - a.verifiedAt);
|
|
584
|
+
let inconclusive = null;
|
|
570
585
|
for (const { slotId } of candidates) {
|
|
571
586
|
const takenDir = join(dirs.taken, slotId);
|
|
572
587
|
if (!(await claim(join(dirs.ready, slotId), takenDir))) continue;
|
|
@@ -582,8 +597,23 @@ export function createSessionPool({
|
|
|
582
597
|
continue;
|
|
583
598
|
}
|
|
584
599
|
const result = await signIn.refresh({ target, storedSession: slot.storedSession, persona });
|
|
600
|
+
if (!result.ok && result.transient) {
|
|
601
|
+
// BOT-2952: Auth did not answer, which says nothing about this slot. It goes back to
|
|
602
|
+
// ready unchanged (the tick does the same, BOT-2947) and the next slot is tried; a
|
|
603
|
+
// persona has at most 4. Nothing unverified is ever handed out.
|
|
604
|
+
inconclusive = result.reason;
|
|
605
|
+
await fs.rm(join(takenDir, "claim.json"), { force: true });
|
|
606
|
+
try {
|
|
607
|
+
await fs.rename(takenDir, join(dirs.ready, slotId));
|
|
608
|
+
} catch (error) {
|
|
609
|
+
// A tick purged it between the claim marker's removal and the rename.
|
|
610
|
+
if (error?.code !== "ENOENT") throw error;
|
|
611
|
+
}
|
|
612
|
+
logger({ event: "take_inconclusive", slotId, persona, reason: result.reason });
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
585
615
|
if (!result.ok) {
|
|
586
|
-
logger({ event: "take_discard", slotId, persona, reason: result.reason, transient:
|
|
616
|
+
logger({ event: "take_discard", slotId, persona, reason: result.reason, transient: false });
|
|
587
617
|
await removeSlot(takenDir);
|
|
588
618
|
continue;
|
|
589
619
|
}
|
|
@@ -602,7 +632,11 @@ export function createSessionPool({
|
|
|
602
632
|
logger({ event: "take_ok", slotId, persona, owner: meta.owner });
|
|
603
633
|
return { slotId, statePath: join(takenDir, "state.json"), dir: takenDir, meta };
|
|
604
634
|
}
|
|
605
|
-
|
|
635
|
+
const empty = new PoolEmptyError(persona, tickIntervalHint);
|
|
636
|
+
if (inconclusive) {
|
|
637
|
+
empty.detail = `Staging Auth did not answer the liveness check (${inconclusive}); the ready sessions were kept for a retry.`;
|
|
638
|
+
}
|
|
639
|
+
throw empty;
|
|
606
640
|
}
|
|
607
641
|
|
|
608
642
|
async function discardTaken(slotId) {
|
|
@@ -16,12 +16,17 @@ import { assertPersonaAllowed } from "./policy.mjs";
|
|
|
16
16
|
import { decodeJwtClaims, toStoredSession } from "./state-file.mjs";
|
|
17
17
|
|
|
18
18
|
export class SignInError extends Error {
|
|
19
|
-
constructor(code, message, { transient = false, status } = {}) {
|
|
19
|
+
constructor(code, message, { transient = false, status, passwordGrantSucceeded, rateLimited } = {}) {
|
|
20
20
|
super(message);
|
|
21
21
|
this.name = "SignInError";
|
|
22
22
|
this.code = code;
|
|
23
23
|
this.transient = transient;
|
|
24
24
|
this.status = status;
|
|
25
|
+
// BOT-2952: set on mint_retries_exhausted. false = no attempt got a password grant, so no
|
|
26
|
+
// AAL1 session existed and no MFA verify ran (the pool retries next tick, no backoff).
|
|
27
|
+
if (passwordGrantSucceeded !== undefined) this.passwordGrantSucceeded = passwordGrantSucceeded;
|
|
28
|
+
// A 429 on any attempt: Auth answered "slow down", so the pool keeps its backoff.
|
|
29
|
+
if (rateLimited !== undefined) this.rateLimited = rateLimited;
|
|
25
30
|
}
|
|
26
31
|
}
|
|
27
32
|
|
|
@@ -93,9 +98,12 @@ export function createGoTrueTotpSignIn({
|
|
|
93
98
|
async function mint({ target, persona }) {
|
|
94
99
|
assertPersonaAllowed(target, persona);
|
|
95
100
|
let lastFailure = "no attempt made";
|
|
101
|
+
let passwordGrantSucceeded = false;
|
|
102
|
+
let rateLimited = false;
|
|
96
103
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
97
|
-
const retry = async (reason, { waitWindow = false } = {}) => {
|
|
104
|
+
const retry = async (reason, { waitWindow = false, response } = {}) => {
|
|
98
105
|
lastFailure = reason;
|
|
106
|
+
if (response?.status === 429) rateLimited = true;
|
|
99
107
|
logger({ event: "mint_retry", persona, attempt, reason });
|
|
100
108
|
if (attempt < maxAttempts) {
|
|
101
109
|
if (waitWindow) await waitForNextTotpWindow();
|
|
@@ -108,7 +116,7 @@ export function createGoTrueTotpSignIn({
|
|
|
108
116
|
});
|
|
109
117
|
if (password.status !== 200) {
|
|
110
118
|
if (isTransient(password)) {
|
|
111
|
-
await retry(`password grant ${describeFailure(password)}
|
|
119
|
+
await retry(`password grant ${describeFailure(password)}`, { response: password });
|
|
112
120
|
continue;
|
|
113
121
|
}
|
|
114
122
|
if (isCredentialRefusal(password)) {
|
|
@@ -120,6 +128,7 @@ export function createGoTrueTotpSignIn({
|
|
|
120
128
|
status: password.status,
|
|
121
129
|
});
|
|
122
130
|
}
|
|
131
|
+
passwordGrantSucceeded = true;
|
|
123
132
|
const aal1Token = password.body?.access_token;
|
|
124
133
|
const factor = (password.body?.user?.factors ?? []).find(
|
|
125
134
|
(candidate) => candidate.factor_type === "totp" && candidate.status === "verified",
|
|
@@ -135,7 +144,7 @@ export function createGoTrueTotpSignIn({
|
|
|
135
144
|
continue;
|
|
136
145
|
}
|
|
137
146
|
if (isTransient(challenge)) {
|
|
138
|
-
await retry(`challenge ${describeFailure(challenge)}
|
|
147
|
+
await retry(`challenge ${describeFailure(challenge)}`, { response: challenge });
|
|
139
148
|
continue;
|
|
140
149
|
}
|
|
141
150
|
throw new SignInError("challenge_failed", `MFA challenge refused: ${describeFailure(challenge)}.`, {
|
|
@@ -158,7 +167,7 @@ export function createGoTrueTotpSignIn({
|
|
|
158
167
|
continue;
|
|
159
168
|
}
|
|
160
169
|
if (isTransient(verify)) {
|
|
161
|
-
await retry(`verify ${describeFailure(verify)}
|
|
170
|
+
await retry(`verify ${describeFailure(verify)}`, { response: verify });
|
|
162
171
|
continue;
|
|
163
172
|
}
|
|
164
173
|
throw new SignInError("verify_failed", `MFA verify refused: ${describeFailure(verify)}.`, { status: verify.status });
|
|
@@ -176,6 +185,8 @@ export function createGoTrueTotpSignIn({
|
|
|
176
185
|
}
|
|
177
186
|
throw new SignInError("mint_retries_exhausted", `Mint gave up after ${maxAttempts} attempts (last: ${lastFailure}).`, {
|
|
178
187
|
transient: true,
|
|
188
|
+
passwordGrantSucceeded,
|
|
189
|
+
rateLimited,
|
|
179
190
|
});
|
|
180
191
|
}
|
|
181
192
|
|