@basein/runner 0.3.25 → 0.3.27
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/README.md +27 -26
- package/dist/auth/client.d.ts +4 -1
- package/dist/auth/client.js +83 -14
- package/dist/bin/bir-hooks.js +4 -2
- package/dist/bin/bir.js +153 -37
- package/dist/bin/investigate.d.ts +13 -0
- package/dist/bin/investigate.js +72 -2
- package/dist/bin/scenario-edit.d.ts +5 -0
- package/dist/bin/scenario-edit.js +17 -1
- package/dist/config/adapters/claude-code.d.ts +51 -7
- package/dist/config/adapters/claude-code.js +105 -18
- package/dist/config/generate.d.ts +61 -13
- package/dist/config/generate.js +108 -20
- package/dist/config/resolve.d.ts +5 -0
- package/dist/config/resolve.js +7 -0
- package/dist/control/correlation.js +0 -0
- package/dist/control/daemon.d.ts +28 -0
- package/dist/control/daemon.js +62 -0
- package/dist/control/paths.d.ts +19 -1
- package/dist/control/paths.js +20 -2
- package/dist/control/pin-guard.js +8 -2
- package/dist/control/recorder-main.js +17 -5
- package/dist/control/server.d.ts +75 -11
- package/dist/control/server.js +440 -77
- package/dist/control/transcript.d.ts +64 -3
- package/dist/control/transcript.js +318 -37
- package/dist/jsonrpc/framing.d.ts +18 -3
- package/dist/jsonrpc/framing.js +57 -4
- package/dist/proxy/intercept.d.ts +4 -0
- package/dist/proxy/intercept.js +4 -0
- package/dist/proxy/relay.d.ts +13 -0
- package/dist/proxy/relay.js +12 -1
- package/dist/proxy/session.d.ts +8 -1
- package/dist/proxy/session.js +67 -10
- package/dist/record/recorder.d.ts +6 -1
- package/dist/record/redact.d.ts +18 -4
- package/dist/record/redact.js +538 -79
- package/dist/record/remote-recorder.d.ts +26 -5
- package/dist/record/remote-recorder.js +174 -18
- package/dist/record/truncate.js +3 -1
- package/dist/replay/bash-channel.d.ts +7 -1
- package/dist/replay/bash-channel.js +9 -3
- package/dist/replay/builtins.d.ts +14 -0
- package/dist/replay/builtins.js +460 -95
- package/dist/replay/bundle.d.ts +15 -11
- package/dist/replay/bundle.js +167 -33
- package/dist/replay/controller.d.ts +92 -1
- package/dist/replay/controller.js +246 -47
- package/dist/replay/derive.js +46 -4
- package/dist/replay/handover.d.ts +13 -0
- package/dist/replay/handover.js +18 -0
- package/dist/replay/plan.d.ts +41 -0
- package/dist/replay/plan.js +171 -3
- package/dist/replay/pricing.d.ts +61 -11
- package/dist/replay/pricing.js +135 -42
- package/dist/replay/reads-only.d.ts +48 -0
- package/dist/replay/reads-only.js +161 -0
- package/dist/replay/safety.js +43 -5
- package/dist/upstream/http-client.d.ts +6 -3
- package/dist/upstream/http-client.js +54 -13
- package/dist/upstream/stdio-client.d.ts +7 -0
- package/dist/upstream/stdio-client.js +1 -1
- package/dist/util/atomic-write.d.ts +9 -1
- package/dist/util/atomic-write.js +9 -6
- package/dist/util/service-health.d.ts +7 -0
- package/dist/util/service-health.js +10 -3
- package/package.json +1 -1
- package/production.md +2 -2
- package/scripts/install.ps1 +14 -2
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
A recording MCP **proxy**. It sits between any MCP client (Claude Code, Cursor,
|
|
4
4
|
Codex, an Agent SDK session) and every MCP server that client uses, executes each
|
|
5
5
|
call on the client's behalf, and records the run as a reusable scenario in the
|
|
6
|
-
BaseIn
|
|
6
|
+
Basic2202 service (called BaseIn in the runner's own messages).
|
|
7
7
|
|
|
8
8
|
The design this implements is [docs/BaseInstRunner.md](docs/BaseInstRunner.md).
|
|
9
9
|
This README is the operator's half: how to run it, what it records, and what it
|
|
@@ -36,22 +36,23 @@ $env:BIR_SETUP_TOKEN="<token>"; irm https://api.bi2202.com/install.ps1 | iex
|
|
|
36
36
|
curl -fsSL https://api.bi2202.com/install.sh | BIR_SETUP_TOKEN="<token>" sh # macOS / Linux
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
That installs Node if the machine has none (a private copy, never on PATH),
|
|
39
|
+
That installs Node if the machine has none it can use (a private copy, never on PATH),
|
|
40
40
|
installs this package, installs Claude Code if it is missing, and runs
|
|
41
41
|
`bir setup`: it signs in, stores the service address, wraps the project's MCP
|
|
42
42
|
servers, wires Claude Code's hooks, adds the scenario server, and starts the
|
|
43
43
|
recorder in the background. It ends with the one thing left to do: `claude`.
|
|
44
44
|
Without a token the same line signs in through the browser. Re-running it is
|
|
45
|
-
safe, and is also how you upgrade
|
|
45
|
+
safe, and is also how you upgrade (or `bir update`, then `bir setup` in each
|
|
46
|
+
project). To record another project: `cd` there and
|
|
46
47
|
run `bir setup` (no token needed; the sign-in is kept).
|
|
47
48
|
|
|
48
|
-
The scripts are
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
[
|
|
49
|
+
The scripts are `scripts/install.ps1` and `scripts/install.sh` in this
|
|
50
|
+
package; the API serves them with its own address filled in, at
|
|
51
|
+
<https://api.bi2202.com/install.ps1> and <https://api.bi2202.com/install.sh>. Step by step, and what each line does:
|
|
52
|
+
[the handbook's Getting started](https://docs.bi2202.com/#start). For a fleet — packaging, releases,
|
|
52
53
|
offline installs, updates: [docs/installRun.md](docs/installRun.md).
|
|
53
54
|
|
|
54
|
-
By hand, or when developing BaseInstRunner itself:
|
|
55
|
+
By hand (Node 20 or newer), or when developing BaseInstRunner itself:
|
|
55
56
|
|
|
56
57
|
```bash
|
|
57
58
|
npm i -g @basein/runner # or: npm install && npm run build, in a checkout
|
|
@@ -197,9 +198,9 @@ exits 2; it sets nothing up.
|
|
|
197
198
|
|
|
198
199
|
Those two lines load a LangChain middleware from the Python package
|
|
199
200
|
`langchain-basic2202`, which reports every call to the recorder. That package
|
|
200
|
-
|
|
201
|
-
how-to is
|
|
202
|
-
|
|
201
|
+
is needed too, and it is not out yet: it is not on PyPI. The
|
|
202
|
+
how-to is the handbook's LangChain page,
|
|
203
|
+
[docs.bi2202.com/#langchain](https://docs.bi2202.com/#langchain).
|
|
203
204
|
|
|
204
205
|
### Taking it out
|
|
205
206
|
|
|
@@ -287,7 +288,7 @@ between the two is not guaranteed.
|
|
|
287
288
|
(`bir up --restart`), for the reason in the next section.
|
|
288
289
|
- **Change what the model does.** Tool descriptions are never edited. The
|
|
289
290
|
model-visible changes are one declared schema key (below), the first-party
|
|
290
|
-
`bir` server's `run_scenario` (only with `--replay`), and replay's own
|
|
291
|
+
`bir` server's `run_scenario` and `report_wrong_result` (the agent's way to say a replay's results are wrong; both only with `--replay`), and replay's own
|
|
291
292
|
`[BaseInstRunner calculated replay]` notes when a prompt matches a scenario.
|
|
292
293
|
The `bir` server's three tools that only read a scenario (`scenario_show`,
|
|
293
294
|
`scenario_edits`, `investigate`) and the three for changing one
|
|
@@ -298,11 +299,11 @@ between the two is not guaranteed.
|
|
|
298
299
|
forced past the scenario check (`force`) still asks the person first. Their
|
|
299
300
|
definitions ride along with every request, so `bir scenario editing off`
|
|
300
301
|
takes the six away, and a later setup leaves that off. A bare
|
|
301
|
-
`bir install --replay` lists `run_scenario`
|
|
302
|
+
`bir install --replay` lists only `run_scenario` and `report_wrong_result` until
|
|
302
303
|
`bir scenario editing on` (or `BIR_SCENARIO_TOOLS=read` in the server's
|
|
303
304
|
environment, for the reading three). With the runner off it lists no tools
|
|
304
305
|
at all.
|
|
305
|
-
- **Record its own `bir` calls
|
|
306
|
+
- **Record its own `bir` calls.** A session
|
|
306
307
|
that fixes a plan is not a task to replay. The `bir` server's tools other
|
|
307
308
|
than `run_scenario`, and a `Bash` or `PowerShell` command made only of `bir`
|
|
308
309
|
commands, are never recorded as steps and never count as leaving a plan.
|
|
@@ -311,7 +312,7 @@ between the two is not guaranteed.
|
|
|
311
312
|
runs here, in the recorder's own process and context (Node's `vm`, not a
|
|
312
313
|
sandbox), next to your sign-in and your wrapped servers. It has a time limit
|
|
313
314
|
(`BIR_LOGIC_TIMEOUT_MS`, 1 s by default) and no memory limit.
|
|
314
|
-
The service
|
|
315
|
+
The service sandboxes its own runs of the same code;
|
|
315
316
|
the runner does not. The service's check of a hand edit only judges what the
|
|
316
317
|
code returns for the recording's values, so it cannot prove what the code
|
|
317
318
|
does here: read a hand edit before it is saved. See
|
|
@@ -379,8 +380,7 @@ the plan is retired and nothing runs. Mixed scenarios steer the live session
|
|
|
379
380
|
instead, pinning each step's arguments. Either way the turn reports what it
|
|
380
381
|
cost, so the savings ledger measures something real.
|
|
381
382
|
|
|
382
|
-
**Fixing a step.**
|
|
383
|
-
`@basein/runner` version and a service deploy. A calculated step is code, and
|
|
383
|
+
**Fixing a step.** From runner 0.3.0. A calculated step is code, and
|
|
384
384
|
code can be wrong. `bir investigate` names such a step; `bir scenario edit`
|
|
385
385
|
fixes it, and the service checks the change against the recording before it
|
|
386
386
|
saves it. **A change is saved only if it reproduces the recording and is not a
|
|
@@ -394,7 +394,7 @@ which `bir setup` turns on (`bir scenario editing on` in a project wired by
|
|
|
394
394
|
[the guide's §9.2](docs/calculatedReplayGuide.md).
|
|
395
395
|
|
|
396
396
|
The service can correct a plan itself too, after the agent did a planned task
|
|
397
|
-
again its own way (
|
|
397
|
+
again its own way (from runner 0.3.16). `bir scenario edits`
|
|
398
398
|
lists such a correction beside the hand edits, under its `scor_` id. Undoing it
|
|
399
399
|
puts back the whole plan from before, and the plan is then held until you
|
|
400
400
|
choose "Use it again" on the recording page.
|
|
@@ -411,7 +411,7 @@ bir scenario calc scn_… --force --discard-edits # recalculate this
|
|
|
411
411
|
bir scenario editing on # let Claude's `bir` tools change scenarios here (setup did)
|
|
412
412
|
```
|
|
413
413
|
|
|
414
|
-
**A value no prompt names.**
|
|
414
|
+
**A value no prompt names.** From runner 0.3.4. Every
|
|
415
415
|
parameter is a *target* (a prompt must name it, or the plan is declined:
|
|
416
416
|
`missing_target`) or a *setting* (its recorded value is used when a prompt
|
|
417
417
|
names none). When the calculation made a database, a host or a working folder a
|
|
@@ -449,7 +449,7 @@ the next one:
|
|
|
449
449
|
BIR_CORRELATION_DECISION=allow bir up --restart
|
|
450
450
|
```
|
|
451
451
|
|
|
452
|
-
`BIR_CORRELATION_DECISION=ask` carries the id and
|
|
452
|
+
`BIR_CORRELATION_DECISION=ask` carries the id and has Claude Code ask about each wrapped call, even one you allowed. Hosts that
|
|
453
453
|
ignore `updatedInput` on a non-allow decision then fall back to fingerprint
|
|
454
454
|
matching.
|
|
455
455
|
|
|
@@ -501,6 +501,7 @@ what to do when they register, so there is one switch and no second copy of it:
|
|
|
501
501
|
| `BIR_REPLAY_BUDGET_MS` / `BIR_STEP_TIMEOUT_MS` | Whole-plan and per-step ceilings for direct execution (default `100000` / `60000`; `BIR_REPLAY_PLAN_BUDGET_MS` also sets the first). A direct run's parameter wait plus its plan are held under 270 s. |
|
|
502
502
|
| `BIR_LOGIC_TIMEOUT_MS` | How long one scenario logic body may run before it is stopped and the plan hands over (default `1000`, at most `10000`). |
|
|
503
503
|
| `BIR_REPLAY_RECORDED_OUTPUTS=1` | Let a step's recorded output stand in when the step could not be sent. Unset, the plan stops at that step and hands the task back. |
|
|
504
|
+
| `BIR_CONTINUATIONS` | After a plan hands the task back, the agent's next call asks the service once for the plan of the work recorded after an earlier hand-over at that step, and that plan does the rest of the task (from 0.3.23). `0` turns it off, and so does `BIR_INTENT_MATCH=0`. |
|
|
504
505
|
|
|
505
506
|
The `bir` server reads its own: `BIR_SCENARIO_TOOLS=read` lists its three reading
|
|
506
507
|
tools where scenario editing is off, and `BIR_SCENARIO_RUN_TIMEOUT_MS` is how
|
|
@@ -531,11 +532,11 @@ The governing rule: **a host session never fails because of BaseInstRunner.**
|
|
|
531
532
|
| Failure | Behaviour |
|
|
532
533
|
|---|---|
|
|
533
534
|
| No recorder running at session start | the SessionStart hook starts one in the background; it always exits 0 and never holds the start longer than about 11.5 s |
|
|
534
|
-
| Control server unreachable at startup | steps buffered
|
|
535
|
+
| Control server unreachable at startup | steps buffered for about 10 s, then Tier 2 (passthrough: nothing); the proxy keeps looking every 15 s and binds when one answers |
|
|
535
536
|
| Control server dies mid-session | the proxy records to a Tier 2 run of its own (flagged lossy), starts the recorder again in the background (not after `bir down`), and binds to it once it answers |
|
|
536
537
|
| A hook the recorder cannot answer in time | every route has a hard deadline (prompt 8.5 s, `PreToolUse` 12 s, the others 1–2 s) and answers `{}`; so does a hook with a stale token or a malformed body |
|
|
537
|
-
| BaseIn service slow or down | hooks stop waiting on it after one failure (a cool-down of 5 s, doubling to 60 s); sends go on in the background, retried once only when they cannot have arrived (429, 503, a refused connection); past 500 queued sends or 32 MiB the oldest steps are dropped |
|
|
538
|
-
| BaseIn service answers `429`: the account is over one of its limits | the send — a run's create, a step, a report — goes once more, at most 2 s later, and is then dropped, with `recorder.send_failed … HTTP 429` on the log (one line per run). A dropped step is a hole in its recording; a run whose create was refused runs without its plan; a `429` the second try did not cure starts the cool-down above. The limits are settings of the service, per account — by default 120 runs and 3,000 steps a minute (a tool call is two steps), 10 title requests a minute, and 20 Calculate, Prefer and Recalculate requests in any one hour
|
|
538
|
+
| BaseIn service slow or down | hooks stop waiting on it after one failure (a cool-down of 5 s, doubling to 60 s); sends go on in the background, retried once only when they cannot have arrived (429, 503, a refused connection); past 500 queued sends or 32 MiB the oldest steps are dropped, and so is a send that has waited a minute for a service still down |
|
|
539
|
+
| BaseIn service answers `429`: the account is over one of its limits | the send — a run's create, a step, a report — goes once more, at most 2 s later, and is then dropped, with `recorder.send_failed … HTTP 429` on the log (one line per run). A dropped step is a hole in its recording; a run whose create was refused runs without its plan; a `429` the second try did not cure starts the cool-down above. The limits are settings of the service, per account — by default 120 runs and 3,000 steps a minute (a tool call is two steps), 10 title requests a minute, and 20 Calculate, Prefer and Recalculate requests in any one hour |
|
|
539
540
|
| Upstream fails to start, or exits | calls in flight are answered (`MCP server "<name>" is not available`, `isError: true` for a `tools/call`), then `bir-proxy` exits with the server's own status, so Claude Code shows the server as failed, as it would unwrapped. `BIR_PROXY_STAY_UP=1` keeps the proxy up answering that instead |
|
|
540
541
|
| Remote upstream unreachable or refusing | the request gets a short error (`upstream unreachable: …`, `upstream requires sign-in (HTTP 401)`), never the server's body; only connecting is bounded (10 s), never how long a tool runs |
|
|
541
542
|
| Result over 128 MiB | the call ends with an error saying the result was too large to deliver, instead of hanging |
|
|
@@ -551,7 +552,7 @@ The governing rule: **a host session never fails because of BaseInstRunner.**
|
|
|
551
552
|
project, and uninstalls nothing: hooks answer with nothing within about a
|
|
552
553
|
second, in sessions already open too, and nothing is recorded, replayed or sent;
|
|
553
554
|
wrapped MCP servers run as plain pipes to the real server from their next start
|
|
554
|
-
(a new session, or `/mcp` → reconnect), and
|
|
555
|
+
(a new session, or `/mcp` → reconnect), and record nothing until then; the `bir`
|
|
555
556
|
server lists no tools. `bir off --here` does the same for one project (from a
|
|
556
557
|
subdirectory, the installed project it is in), and `bir on` or `bir on --here`
|
|
557
558
|
undoes it. If the problem goes away, the runner was the cause; if it stays, the
|
|
@@ -575,7 +576,7 @@ environment-variable reference, the operations runbook and the findings ledger.
|
|
|
575
576
|
|
|
576
577
|
```bash
|
|
577
578
|
npm run build # src → dist
|
|
578
|
-
npm test # ~1,
|
|
579
|
+
npm test # ~1,300 tests: transcript identity, passthrough, correlation, config, replay, setup
|
|
579
580
|
npm run typecheck
|
|
580
581
|
npm run test:smoke # end-to-end replay with real processes (needs build + pretest first)
|
|
581
582
|
```
|
|
@@ -584,7 +585,7 @@ The core test is **transcript identity**: the same scripted conversation is run
|
|
|
584
585
|
against a fake upstream directly and through the proxy, and every host-visible
|
|
585
586
|
message must match. If a change breaks transparency, that test says so first.
|
|
586
587
|
|
|
587
|
-
`test/smoke-replay.mjs` is the other one worth running by hand.
|
|
588
|
+
`test/smoke-replay.mjs` is the other one worth running by hand. Most of the suite
|
|
588
589
|
runs in-process; this starts a stand-in BaseIn service, a real `bir-hooks`, a real
|
|
589
590
|
`bir-proxy` wrapping a real MCP server and a real `bir-scenario`, then drives a
|
|
590
591
|
two-step scenario through actual stdio. It is what catches the seams between
|
package/dist/auth/client.d.ts
CHANGED
|
@@ -12,7 +12,10 @@
|
|
|
12
12
|
*
|
|
13
13
|
* 1. Load the cached session from ~/.baseinstrunner/credentials.json.
|
|
14
14
|
* 2. If the access token is still valid, reuse it.
|
|
15
|
-
* 3. Else if a refresh token is present, POST /auth/refresh (silent).
|
|
15
|
+
* 3. Else if a refresh token is present, POST /auth/refresh (silent). A
|
|
16
|
+
* refusal is checked against the file before it is believed: the service
|
|
17
|
+
* rotates refresh tokens, and a sibling process may have spent this one
|
|
18
|
+
* a moment ago and saved the session it got for it.
|
|
16
19
|
* 4. Else give up and return undefined.
|
|
17
20
|
*
|
|
18
21
|
* It used to have a fifth step — prompt for a password — and that had to go
|
package/dist/auth/client.js
CHANGED
|
@@ -12,7 +12,10 @@
|
|
|
12
12
|
*
|
|
13
13
|
* 1. Load the cached session from ~/.baseinstrunner/credentials.json.
|
|
14
14
|
* 2. If the access token is still valid, reuse it.
|
|
15
|
-
* 3. Else if a refresh token is present, POST /auth/refresh (silent).
|
|
15
|
+
* 3. Else if a refresh token is present, POST /auth/refresh (silent). A
|
|
16
|
+
* refusal is checked against the file before it is believed: the service
|
|
17
|
+
* rotates refresh tokens, and a sibling process may have spent this one
|
|
18
|
+
* a moment ago and saved the session it got for it.
|
|
16
19
|
* 4. Else give up and return undefined.
|
|
17
20
|
*
|
|
18
21
|
* It used to have a fifth step — prompt for a password — and that had to go
|
|
@@ -44,7 +47,7 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync } from "node
|
|
|
44
47
|
import { hostname } from "node:os";
|
|
45
48
|
import { configDir, configPath, ensureDir } from "../control/paths.js";
|
|
46
49
|
import { join } from "node:path";
|
|
47
|
-
import { logLine, errText } from "../util/log.js";
|
|
50
|
+
import { logDetail, logLine, errText } from "../util/log.js";
|
|
48
51
|
import { packageVersion } from "../util/version.js";
|
|
49
52
|
import { serviceHeaders } from "../util/service-headers.js";
|
|
50
53
|
import { writeFileAtomic } from "../util/atomic-write.js";
|
|
@@ -536,6 +539,22 @@ async function tryRefresh(authUrl, refreshToken) {
|
|
|
536
539
|
return { rejected };
|
|
537
540
|
}
|
|
538
541
|
}
|
|
542
|
+
/**
|
|
543
|
+
* After a refused refresh, how long to wait before the second look at
|
|
544
|
+
* credentials.json: the sibling that spent the token a moment ago has the
|
|
545
|
+
* service's answer and may not have written it yet. The recorder waits the
|
|
546
|
+
* same (record/remote-recorder.ts).
|
|
547
|
+
*/
|
|
548
|
+
const REFUSED_RECHECK_MS = 1_500;
|
|
549
|
+
/**
|
|
550
|
+
* The session in credentials.json, when it is one for `authUrl` that carries
|
|
551
|
+
* another refresh token than `spent`: what a sibling's refresh, or a new
|
|
552
|
+
* sign-in, saved after this process read the file.
|
|
553
|
+
*/
|
|
554
|
+
function savedSince(authUrl, spent) {
|
|
555
|
+
const stored = loadCredentials();
|
|
556
|
+
return stored && stored.refreshToken !== spent && issuedFor(stored, authUrl) ? stored : undefined;
|
|
557
|
+
}
|
|
539
558
|
/**
|
|
540
559
|
* Authenticate and return a valid session, or `undefined` when none can be
|
|
541
560
|
* obtained. Callers degrade to not-recording rather than failing the session.
|
|
@@ -571,25 +590,75 @@ export async function authenticate(opts) {
|
|
|
571
590
|
if (cached && isAccessValid(cached))
|
|
572
591
|
return cached;
|
|
573
592
|
if (cached?.refreshToken) {
|
|
574
|
-
|
|
593
|
+
// The session whose refresh token is being spent: the cached one, or the
|
|
594
|
+
// one a sibling wrote while ours was being refused.
|
|
595
|
+
let spent = cached;
|
|
596
|
+
let { session: refreshed, rejected } = await tryRefresh(authUrl, spent.refreshToken);
|
|
597
|
+
if (rejected) {
|
|
598
|
+
// The service rotates refresh tokens — each works once — and every
|
|
599
|
+
// process reads the same file. Two proxies that start together on an
|
|
600
|
+
// expired access token both read r1; the one whose refresh arrives
|
|
601
|
+
// second is refused, while the winner's session is in the file or about
|
|
602
|
+
// to be. Believing that refusal left this process signed out for its
|
|
603
|
+
// whole life beside a valid session: a proxy recorded nothing, and the
|
|
604
|
+
// CLI said "run `bir login`". So the file is read again — now, and once
|
|
605
|
+
// more when the winner has had time to write — as the recorder does
|
|
606
|
+
// (record/remote-recorder.ts, refresh).
|
|
607
|
+
let newer = savedSince(authUrl, spent.refreshToken);
|
|
608
|
+
if (!newer) {
|
|
609
|
+
await defaultSleep(REFUSED_RECHECK_MS);
|
|
610
|
+
newer = savedSince(authUrl, spent.refreshToken);
|
|
611
|
+
}
|
|
612
|
+
if (newer) {
|
|
613
|
+
logDetail("auth.adopted", { why: "another process had already refreshed" });
|
|
614
|
+
if (isAccessValid(newer))
|
|
615
|
+
return newer;
|
|
616
|
+
// Newer, and expired all the same. Its refresh token is not the one
|
|
617
|
+
// that was just refused, so it gets its one try.
|
|
618
|
+
spent = newer;
|
|
619
|
+
({ session: refreshed, rejected } = await tryRefresh(authUrl, spent.refreshToken));
|
|
620
|
+
}
|
|
621
|
+
}
|
|
575
622
|
if (!refreshed && !rejected && opts?.keepStaleOnNoAnswer) {
|
|
576
623
|
logLine("auth.refresh_deferred", {
|
|
577
624
|
why: "the service did not answer the refresh — starting with the expired session; it is renewed once the service is back",
|
|
578
625
|
});
|
|
579
|
-
return
|
|
626
|
+
return spent;
|
|
580
627
|
}
|
|
581
628
|
if (refreshed) {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
629
|
+
// Saved only while the file still holds the token this refresh spent
|
|
630
|
+
// (the keeper's rule: control/recorder-keeper.ts, saveRefreshed). A
|
|
631
|
+
// refresh can take seconds, and `bir logout` or a sign-in as someone
|
|
632
|
+
// else may finish meanwhile; saving over that signed the machine back
|
|
633
|
+
// in, or put the account just left back in the file.
|
|
634
|
+
//
|
|
635
|
+
// A file that is there and cannot be read (a lock, a moment's torn
|
|
636
|
+
// write) is no sign-in to protect: it is written over, as the recorder
|
|
637
|
+
// does (record/remote-recorder.ts). Taken for a sign-out, it lost the
|
|
638
|
+
// session the service had just rotated to, and left the dead token on
|
|
639
|
+
// disk: every process signed out until `bir login`.
|
|
640
|
+
const onDisk = loadCredentials();
|
|
641
|
+
const unreadable = onDisk === undefined && existsSync(credentialsPath());
|
|
642
|
+
if (unreadable || onDisk?.refreshToken === spent.refreshToken) {
|
|
643
|
+
try {
|
|
644
|
+
saveCredentials(refreshed);
|
|
645
|
+
}
|
|
646
|
+
catch (err) {
|
|
647
|
+
// The service has already rotated the refresh token, so the one on
|
|
648
|
+
// disk is dead either way. Throwing here would also lose the new one
|
|
649
|
+
// and kill the recorder at start; returning it keeps this process
|
|
650
|
+
// recording, and the next save (a later refresh) repairs the file.
|
|
651
|
+
logLine("auth.save_failed", { error: errText(err), why: "the new session still works in this process" });
|
|
652
|
+
}
|
|
653
|
+
return refreshed;
|
|
591
654
|
}
|
|
592
|
-
|
|
655
|
+
// Not ours to overwrite, and not ours to use either: this process would
|
|
656
|
+
// act for an account the machine has left. What is on disk decides.
|
|
657
|
+
logDetail("auth.refresh_superseded", {
|
|
658
|
+
why: "the sign-in on disk changed while a refresh was out; what is on disk now decides",
|
|
659
|
+
});
|
|
660
|
+
if (onDisk && issuedFor(onDisk, authUrl) && isAccessValid(onDisk))
|
|
661
|
+
return onDisk;
|
|
593
662
|
}
|
|
594
663
|
}
|
|
595
664
|
// No session, and this function does not make one. Signing in is a browser
|
package/dist/bin/bir-hooks.js
CHANGED
|
@@ -139,7 +139,7 @@ import { dirname, resolve } from "node:path";
|
|
|
139
139
|
import { Writable } from "node:stream";
|
|
140
140
|
import { ENSURE_ARG } from "../config/adapters/claude-code.js";
|
|
141
141
|
import { exemptLoopbackFromProxy } from "../config/generate.js";
|
|
142
|
-
import { REVIVE_ARG } from "../control/daemon.js";
|
|
142
|
+
import { REVIVE_ARG, exitLogged } from "../control/daemon.js";
|
|
143
143
|
import { daemonLogPath, ensureDir } from "../control/paths.js";
|
|
144
144
|
import { errText, logLine } from "../util/log.js";
|
|
145
145
|
/**
|
|
@@ -263,6 +263,8 @@ void main().catch((err) => {
|
|
|
263
263
|
process.exit(0);
|
|
264
264
|
}
|
|
265
265
|
logLine("control.fatal", { error: errText(err) });
|
|
266
|
-
process.exit(1)
|
|
266
|
+
// Not `process.exit(1)`: the recorder's log is written in the background,
|
|
267
|
+
// and the line above — the reason it stopped — would be lost with the rest.
|
|
268
|
+
exitLogged(1);
|
|
267
269
|
});
|
|
268
270
|
//# sourceMappingURL=bir-hooks.js.map
|