@basein/runner 0.2.12 → 0.3.0

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.
Files changed (142) hide show
  1. package/README.md +540 -369
  2. package/dist/auth/client.d.ts +65 -3
  3. package/dist/auth/client.js +305 -53
  4. package/dist/bin/bir-hooks.d.ts +72 -31
  5. package/dist/bin/bir-hooks.js +176 -284
  6. package/dist/bin/bir-proxy.d.ts +17 -2
  7. package/dist/bin/bir-proxy.js +222 -28
  8. package/dist/bin/bir-scenario.d.ts +61 -21
  9. package/dist/bin/bir-scenario.js +593 -122
  10. package/dist/bin/bir.d.ts +12 -6
  11. package/dist/bin/bir.js +2935 -489
  12. package/dist/bin/investigate.js +40 -15
  13. package/dist/bin/scenario-edit.d.ts +7 -1
  14. package/dist/bin/scenario-edit.js +43 -21
  15. package/dist/bin/scenario-liveness.d.ts +194 -0
  16. package/dist/bin/scenario-liveness.js +459 -0
  17. package/dist/bin/scenario-stderr.d.ts +43 -0
  18. package/dist/bin/scenario-stderr.js +76 -0
  19. package/dist/bin/service-read.d.ts +88 -0
  20. package/dist/bin/service-read.js +128 -0
  21. package/dist/bin/setup.d.ts +19 -0
  22. package/dist/bin/setup.js +146 -20
  23. package/dist/config/adapters/claude-code.d.ts +92 -14
  24. package/dist/config/adapters/claude-code.js +291 -101
  25. package/dist/config/adapters/generic.d.ts +4 -1
  26. package/dist/config/adapters/generic.js +13 -14
  27. package/dist/config/generate.d.ts +392 -14
  28. package/dist/config/generate.js +666 -19
  29. package/dist/config/mcp-approval.d.ts +76 -0
  30. package/dist/config/mcp-approval.js +210 -0
  31. package/dist/config/node-path.d.ts +42 -0
  32. package/dist/config/node-path.js +102 -0
  33. package/dist/config/overrides.d.ts +62 -0
  34. package/dist/config/overrides.js +136 -0
  35. package/dist/config/resolve.d.ts +70 -8
  36. package/dist/config/resolve.js +186 -15
  37. package/dist/control/client.d.ts +22 -0
  38. package/dist/control/client.js +26 -7
  39. package/dist/control/correlation.d.ts +23 -5
  40. package/dist/control/correlation.js +0 -0
  41. package/dist/control/daemon.d.ts +400 -18
  42. package/dist/control/daemon.js +1105 -102
  43. package/dist/control/discovery.d.ts +77 -5
  44. package/dist/control/discovery.js +186 -11
  45. package/dist/control/ensure-hook.d.ts +61 -0
  46. package/dist/control/ensure-hook.js +160 -8
  47. package/dist/control/paths.d.ts +26 -0
  48. package/dist/control/paths.js +34 -0
  49. package/dist/control/pin-guard.d.ts +83 -0
  50. package/dist/control/pin-guard.js +534 -0
  51. package/dist/control/recorder-keeper.d.ts +229 -0
  52. package/dist/control/recorder-keeper.js +721 -0
  53. package/dist/control/recorder-main.d.ts +73 -0
  54. package/dist/control/recorder-main.js +778 -0
  55. package/dist/control/server.d.ts +707 -31
  56. package/dist/control/server.js +2786 -302
  57. package/dist/control/transcript.d.ts +21 -1
  58. package/dist/control/transcript.js +378 -112
  59. package/dist/index.d.ts +5 -1
  60. package/dist/index.js +5 -1
  61. package/dist/jsonrpc/framing.d.ts +76 -12
  62. package/dist/jsonrpc/framing.js +216 -59
  63. package/dist/proxy/intercept.d.ts +49 -5
  64. package/dist/proxy/intercept.js +188 -59
  65. package/dist/proxy/relay.d.ts +16 -0
  66. package/dist/proxy/relay.js +77 -5
  67. package/dist/proxy/revive.d.ts +122 -0
  68. package/dist/proxy/revive.js +251 -0
  69. package/dist/proxy/session.d.ts +222 -14
  70. package/dist/proxy/session.js +708 -99
  71. package/dist/record/recorder.d.ts +51 -3
  72. package/dist/record/recorder.js +3 -0
  73. package/dist/record/redact.d.ts +103 -9
  74. package/dist/record/redact.js +659 -30
  75. package/dist/record/remote-recorder.d.ts +327 -32
  76. package/dist/record/remote-recorder.js +1517 -187
  77. package/dist/record/switchable-recorder.d.ts +126 -0
  78. package/dist/record/switchable-recorder.js +351 -0
  79. package/dist/record/truncate.d.ts +40 -1
  80. package/dist/record/truncate.js +111 -4
  81. package/dist/replay/bash-channel.d.ts +35 -0
  82. package/dist/replay/bash-channel.js +133 -0
  83. package/dist/replay/bundle.d.ts +10 -0
  84. package/dist/replay/bundle.js +10 -0
  85. package/dist/replay/controller.d.ts +369 -20
  86. package/dist/replay/controller.js +1176 -107
  87. package/dist/replay/derive.d.ts +1 -0
  88. package/dist/replay/derive.js +81 -24
  89. package/dist/replay/executor.d.ts +51 -16
  90. package/dist/replay/executor.js +114 -41
  91. package/dist/replay/handover.d.ts +54 -1
  92. package/dist/replay/handover.js +55 -5
  93. package/dist/replay/logic.d.ts +62 -10
  94. package/dist/replay/logic.js +140 -22
  95. package/dist/replay/plan.d.ts +201 -27
  96. package/dist/replay/plan.js +581 -141
  97. package/dist/replay/safety.d.ts +72 -0
  98. package/dist/replay/safety.js +303 -0
  99. package/dist/replay/source-run.d.ts +10 -0
  100. package/dist/replay/source-run.js +80 -35
  101. package/dist/replay/targets.d.ts +15 -1
  102. package/dist/replay/targets.js +62 -4
  103. package/dist/replay/tool-error.d.ts +15 -1
  104. package/dist/replay/tool-error.js +33 -1
  105. package/dist/replay/types.d.ts +7 -4
  106. package/dist/upstream/client.d.ts +56 -3
  107. package/dist/upstream/client.js +61 -4
  108. package/dist/upstream/headers.d.ts +43 -0
  109. package/dist/upstream/headers.js +90 -0
  110. package/dist/upstream/http-client.d.ts +213 -22
  111. package/dist/upstream/http-client.js +1034 -168
  112. package/dist/upstream/http-transport.d.ts +143 -0
  113. package/dist/upstream/http-transport.js +391 -0
  114. package/dist/upstream/lazy-client.d.ts +8 -0
  115. package/dist/upstream/lazy-client.js +29 -1
  116. package/dist/upstream/sse.d.ts +78 -0
  117. package/dist/upstream/sse.js +189 -0
  118. package/dist/upstream/stdio-client.d.ts +61 -1
  119. package/dist/upstream/stdio-client.js +356 -33
  120. package/dist/util/atomic-write.d.ts +29 -0
  121. package/dist/util/atomic-write.js +96 -0
  122. package/dist/util/journal.d.ts +15 -0
  123. package/dist/util/journal.js +48 -3
  124. package/dist/util/log.d.ts +10 -0
  125. package/dist/util/log.js +28 -1
  126. package/dist/util/off-switch.d.ts +46 -0
  127. package/dist/util/off-switch.js +91 -0
  128. package/dist/util/service-health.d.ts +133 -0
  129. package/dist/util/service-health.js +217 -0
  130. package/docs/BaseInstRunner.md +694 -621
  131. package/docs/calculatedReplay.md +1625 -1304
  132. package/docs/calculatedReplayGuide.md +1212 -1087
  133. package/docs/installRun.md +1013 -772
  134. package/docs/loginWeb.md +627 -607
  135. package/docs/mcpmark.md +49 -16
  136. package/docs/my-first-sample.md +569 -545
  137. package/docs/quickstart.md +384 -311
  138. package/docs/t-bench.md +406 -394
  139. package/package.json +58 -57
  140. package/production.md +1371 -0
  141. package/scripts/install.ps1 +97 -6
  142. package/scripts/install.sh +104 -7
package/README.md CHANGED
@@ -1,369 +1,540 @@
1
- # BaseInstRunnerMCP
2
-
3
- A recording MCP **proxy**. It sits between any MCP client (Claude Code, Cursor,
4
- Codex, an Agent SDK session) and every MCP server that client uses, executes each
5
- call on the client's behalf, and records the run as a reusable scenario in the
6
- BaseIn service.
7
-
8
- The design this implements is [docs/BaseInstRunner.md](docs/BaseInstRunner.md).
9
- This README is the operator's half: how to run it, what it records, and what it
10
- cannot. For v2 — running a calculated scenario when a prompt means the same thing
11
- as one you have run before — see [docs/calculatedReplay.md](docs/calculatedReplay.md)
12
- and its runbook [docs/calculatedReplayGuide.md](docs/calculatedReplayGuide.md).
13
-
14
- ```
15
- model ──mcp__chrome-devtools__*──▶ bir-proxy ──stdio/http──▶ chrome-devtools-mcp
16
- hooks ──built-ins, prompt, stop──▶ bir-hooks (control server) ──▶ BaseIn /recordings/*
17
- ▲
18
- bir-proxy ────┘ step reports
19
- ```
20
-
21
- ---
22
-
23
- ## Install
24
-
25
- One line, pasted into a terminal **opened in the project you start Claude Code
26
- in**. The console's *Set up the runner* page shows it with a one-time setup
27
- token filled in, so there is no sign-in step either:
28
-
29
- ```powershell
30
- $env:BIR_SETUP_TOKEN="<token>"; irm https://api.bi2202.com/install.ps1 | iex # Windows (PowerShell)
31
- ```
32
-
33
- ```bash
34
- curl -fsSL https://api.bi2202.com/install.sh | BIR_SETUP_TOKEN="<token>" sh # macOS / Linux
35
- ```
36
-
37
- That installs Node if the machine has none (a private copy, never on PATH),
38
- installs this package, installs Claude Code if it is missing, and runs
39
- `bir setup`: it signs in, stores the service address, wraps the project's MCP
40
- servers, wires Claude Code's hooks, adds the scenario server, and starts the
41
- recorder in the background. It ends with the one thing left to do: `claude`.
42
- Without a token the same line signs in through the browser. Re-running it is
43
- safe, and is also how you upgrade. To record another project: `cd` there and
44
- run `bir setup` (no token needed; the sign-in is kept).
45
-
46
- The scripts are [scripts/install.ps1](scripts/install.ps1) and
47
- [scripts/install.sh](scripts/install.sh); the API serves them with its own
48
- address filled in. Step by step, and what each line does:
49
- [docs/quickstart.md](docs/quickstart.md). For a fleet — packaging, releases,
50
- offline installs, updates: [docs/installRun.md](docs/installRun.md).
51
-
52
- By hand, or when developing BaseInstRunner itself:
53
-
54
- ```bash
55
- npm i -g @basein/runner # or: npm install && npm run build, in a checkout
56
- cd the-project-you-record-from
57
- bir setup --auth-url https://api.bi2202.com # everything below, in one go
58
- ```
59
-
60
- or the pieces `bir setup` is made of:
61
-
62
- ```bash
63
- bir login # once; approve it in the browser (or --token <t> from the console)
64
- bir install --global --replay # --local from a checkout; plain `bir install` writes a pinned npx entry
65
- bir up # the recorder, in the background; the SessionStart hook does this too
66
- claude # your session, now recorded
67
- ```
68
-
69
- `bir install` rewrites your MCP entries in place, keeping every key (so
70
- `mcp__<server>__<tool>` names never change), and wires Claude Code's hooks into
71
- `.claude/settings.local.json` — your file, not the one a team commits; in a git
72
- repository it is added to `.git/info/exclude`. It stashes the original text of
73
- every file first — `bir uninstall` puts a file back byte-for-byte when nothing
74
- else has edited it since, and repairs it entry by entry when something has. A
75
- project remembers whether it was installed with `--global`, `--local` or the
76
- pinned-npx default, so a later `bir install` with no flag keeps that shape.
77
-
78
- **Nothing has to stay open.** The SessionStart hook is `bir-hooks ensure`: it
79
- starts the recorder for the project when none is running, in the background,
80
- with its audit log at `~/.baseinstrunner/logs/<project-key>.log`. `bir up`
81
- starts it by hand, `bir down` stops it, `bir status` shows it. Running
82
- `bir-hooks` in a terminal still works and is what the hook does for you.
83
-
84
- The service address is stored by `bir setup` and `bir login` in
85
- `~/.baseinstrunner/config.json`; `BIR_AUTH_URL` in the environment overrides
86
- it. A cached session is bound to the service that issued it and is never sent
87
- anywhere else.
88
-
89
- `bir login` prints a link and a short code and waits for you to approve it in
90
- the browser — the OAuth device grant, the same shape `gh auth login` uses. It
91
- never handles a password, so it works for accounts that only sign in with
92
- Google, and the link can be opened on any device, which is what makes it usable
93
- on a headless runner. `--no-browser` prints the link without opening one;
94
- `--token <value>` redeems a one-time setup token made on the console's *Set up
95
- the runner* page, with no browser step at all (service 2026-09 or newer);
96
- `--password` is the deprecated email-and-password prompt, removed next release.
97
-
98
- Check it:
99
-
100
- ```bash
101
- bir status # what is wrapped, and which config scope won
102
- bir doctor # is it working *right now* — exits non-zero if not
103
- ```
104
-
105
- ### Any other MCP client
106
-
107
- Nothing here knows where Cursor or Codex keep their settings, and it does not
108
- guess. Two paths:
109
-
110
- ```bash
111
- bir install --config /path/to/that/client/mcp.json
112
- bir wrap --server-name my-server -- node my-server.js # prints an entry to paste
113
- ```
114
-
115
- ---
116
-
117
- ## The two tiers
118
-
119
- Only Claude Code has hooks, so run ownership is negotiated at proxy startup.
120
-
121
- | Tier | When | Records |
122
- |---|---|---|
123
- | **1 — Bound** | a control server (`bir-hooks`) is discoverable for this cwd | built-ins **and** MCP, one ordered step stream, prompt and final answer |
124
- | **2 — Standalone** | no control server within 5 s | MCP calls only — no prompt, no final answer, no built-in steps |
125
-
126
- Tier 2 is not a bug; it is the honest ceiling of what an MCP proxy can observe. A
127
- run records which tier produced it in `metadata.tier`, so nothing downstream
128
- mistakes a partial trace for a complete one.
129
-
130
- Steps taken while the tier is still being decided are **buffered**, not dropped —
131
- the host may spawn MCP servers before `SessionStart` fires, and the ordering
132
- between the two is not guaranteed.
133
-
134
- ---
135
-
136
- ## What it will not do
137
-
138
- - **Non-MCP tools without the hook binary.** `Bash`, `Read`, `Edit` and friends
139
- are not MCP traffic. An MCP server sees only MCP traffic addressed to it. That
140
- is a structural fact, not a limitation of this implementation.
141
- - **`claude-in-chrome`.** It is `scope: "dynamic"`, present in no config file, so
142
- there is no entry to rewrite.
143
- - **Replay, unless you install it.** Recording is the whole of what runs out of
144
- the box. Calculated replay — running a matched prompt's scenario instead of
145
- rediscovering it — ships, but needs `bir install --replay`. Once installed it
146
- is on by default; `BIR_REPLAY=0` turns it off, for the reason in the next
147
- section.
148
- - **Change what the model does.** Tool descriptions are never edited. The
149
- model-visible changes are schema relaxation, below, and — only with
150
- `--replay` — the first-party `bir` server's tools: `run_scenario`, and three
151
- that only read a scenario. The three for changing one (`scenario_check`,
152
- `scenario_edit`, `scenario_undo`) appear only after `bir scenario editing on`
153
- in that project (not yet released, see below).
154
- - **Record its own `bir` calls** (next version, not yet released). A session
155
- that fixes a plan is not a task to replay. The `bir` server's tools other
156
- than `run_scenario`, and a `Bash` or `PowerShell` command made only of `bir`
157
- commands, are never recorded as steps and never count as leaving a plan.
158
- `npm test && bir investigate` is still recorded: `npm test` is real work.
159
- - **Sandbox scenario code on this machine.** A calculated or hand-edited step
160
- runs here with `new Function`, next to your sign-in and your wrapped servers.
161
- The service will sandbox its own runs of the same code (with its next deploy);
162
- the runner does not. The service's check of a hand edit only judges what the
163
- code returns for the recording's values, so it cannot prove what the code
164
- does here: read a hand edit before it is saved. See
165
- [the design's §13.1](docs/calculatedReplay.md).
166
-
167
- ---
168
-
169
- ## Calculated replay
170
-
171
- When the service recognises that a prompt means the same thing as one you have
172
- run before, it hands back the *calculated scenario* derived from that earlier
173
- run: an intent, a parameter schema, and one bit of logic per step that recomputes
174
- that step's arguments. `bir` can run it instead of letting the model rediscover
175
- the plan token by token.
176
-
177
- The design is [docs/calculatedReplay.md](docs/calculatedReplay.md); the runbook —
178
- record, calculate, check, enable, read the audit lines — is
179
- [docs/calculatedReplayGuide.md](docs/calculatedReplayGuide.md).
180
-
181
- ```bash
182
- bir install --replay # adds the `bir` MCP server, pre-approved in .claude/settings.local.json
183
- bir up # or let the SessionStart hook start the recorder; replay is on by default
184
- bir replay allow fleet # the servers replay may call unattended — stored for this project
185
- bir replay off # and back on with `bir replay on`; `bir replay status` shows it
186
- ```
187
-
188
- `bir setup` does the first two. The switches are kept per project in
189
- `~/.baseinstrunner/installed.json`, so a recorder the hook starts — with Claude
190
- Code's environment, not your terminal's — keeps them; `BIR_REPLAY`,
191
- `BIR_REPLAY_ALLOW_SERVERS` and `BIR_MIN_STEER_SIMILARITY` in the environment
192
- still override them, and `bir doctor` says which source is in effect.
193
-
194
- Two switches, not one, because of what replay is: **a scenario is a pre-approved
195
- list of tool calls with computed arguments.** A steered step is answered
196
- `permissionDecision: "allow"`, so a pinned `Bash` or `Edit` runs without the
197
- prompt you would have seen. A *directly executed* step never reaches the
198
- permission system at all — it is dispatched to a `bir-proxy` and called on a
199
- connection that is already open. Set `BIR_REPLAY_ALLOW_SERVERS` to the servers
200
- you are content to have called unattended, and read
201
- [the guide's §5.1](docs/calculatedReplayGuide.md) before enabling it anywhere
202
- that matters.
203
-
204
- What it buys: on a scenario whose every step is a wrapped MCP tool, the whole
205
- sequence runs through connections the proxies already hold — **zero model tokens**
206
- — and the model's only job is one tool call that reads the results. Mixed
207
- scenarios steer the live session instead, pinning each step's arguments. Either
208
- way the turn reports what it cost, so the savings ledger measures something real.
209
-
210
- **Fixing a step.** Built 2026-09-25, not yet released: it needs the next
211
- `@basein/runner` version and a service deploy. A calculated step is code, and
212
- code can be wrong. `bir investigate` names such a step; `bir scenario edit`
213
- fixes it, and the service checks the change against the recording before it
214
- saves it. **A change is saved only if it reproduces the recording and is not a
215
- copy — or on purpose, with `--force --note "why"`.** Only the owner can change
216
- a scenario. `bir scenario undo` takes changes back one at a time: run it again
217
- to go one further back. An undo is checked too, and is refused if it would
218
- break a later step or the final answer, unless you add `--force --note "why"`.
219
- Claude can do all of this: in its terminal, or with the `bir` server's tools
220
- once `bir scenario editing on` was run in the project. Step by step, with real
221
- output:
222
- [the guide's §9.2](docs/calculatedReplayGuide.md).
223
-
224
- ```bash
225
- bir scenario show scn_… --step 3 # read one step (scenarioId and chainRevision first)
226
- bir scenario check scn_… --step 3 --input-logic step3.js # try a change; nothing is saved
227
- bir scenario edit scn_… --step 3 --input-logic step3.js --note "why" # the same check, then save
228
- bir scenario edits scn_… # the history, newest first
229
- bir scenario undo scn_… # take back the newest edit; again to go further back
230
- bir scenario undo scn_… --step 3 # take back step 3's newest edit only
231
- bir scenario calc scn_… --force --discard-edits # recalculate this plan, dropping its hand edits
232
- bir scenario editing on # let Claude's `bir` tools change scenarios here
233
- ```
234
-
235
- ---
236
-
237
- ## Two things to know before you turn it on
238
-
239
- **1. Correlation auto-approves wrapped MCP calls.** Merging the hook's view of a
240
- call with the proxy's requires carrying an id through the tool's arguments, and
241
- the mechanism for that is answering `PreToolUse` with
242
- `permissionDecision: "allow"`. That skips a permission prompt you might otherwise
243
- have seen for those tools. To keep the prompt:
244
-
245
- ```bash
246
- BIR_CORRELATION_DECISION=ask bir-hooks
247
- ```
248
-
249
- Hosts that ignore `updatedInput` on a non-allow decision then fall back to
250
- fingerprint matching, which is lossy under identical concurrent calls.
251
-
252
- **2. Schema relaxation is visible to the model.** Carrying that id means adding
253
- `additionalProperties: true` plus one declared key to each wrapped tool's schema,
254
- on every call — not only recorded ones. It is applied **only** in Tier 1 with
255
- correlation active, and **only** to wrapped servers. `--no-correlation` turns it
256
- off entirely and joins on `(server, tool, hash(args))` inside a 30 s window
257
- instead.
258
-
259
- ---
260
-
261
- ## Configuration
262
-
263
- | Variable | Effect |
264
- |---|---|
265
- | `BIR_AUTH_URL` | BaseIn auth-service — the **API** address (normally `https://api.<your-domain>`), not the docs or app website. Overrides the address `bir setup` / `bir login` stored in `~/.baseinstrunner/config.json`. **Neither set means nothing is recorded** — everything else still works. |
266
- | `BIR_SETUP_TOKEN` | A one-time setup token from the console, read by `bir setup` when `--token` is not given; the install scripts pass it this way. Never inherited by the recorder. |
267
- | `BIR_AUTH_DISABLE=1` | Skip authentication (local dev / CI). |
268
- | `BIR_CONTROL_PORT` | Preferred control-server port (default 53411). |
269
- | `BIR_CONTROL_URL` / `BIR_CONTROL_TOKEN` | Point a proxy at a control server directly, bypassing discovery. Injected into generated config; the only reliable channel for an SDK session. |
270
- | `BIR_CORRELATION_DECISION` | `allow` (default) or `ask` — see above. |
271
- | `BIR_NO_CORRELATION=1` | Never inject a call id. |
272
- | `BIR_HOME` | Where state lives (default `~/.baseinstrunner`). |
273
- | `BIR_CLAUDE_JSON` | Override the `~/.claude.json` location. |
274
- | `BIR_VERBOSE=1` | Per-message detail lines on stderr. |
275
-
276
- Calculated replay adds these, all read by `bir-hooks` only — the proxies are told
277
- what to do when they register, so there is one switch and no second copy of it:
278
-
279
- | Variable | Effect |
280
- |---|---|
281
- | `BIR_REPLAY=0` | Disable replay. It is on by default; nothing below matters while this is `0`. |
282
- | `BIR_REPLAY_ALLOW_SERVERS` | Server keys eligible for **direct** execution. Unset means the list `bir replay allow` stored for the project, else every wrapped server; narrowing it is the recommendation. |
283
- | `BIR_MIN_STEER_SIMILARITY` | Minimum match similarity to replay (default `0.92`, above the service's own `0.9` detection threshold). |
284
- | `ANTHROPIC_API_KEY` | **Optional.** Working out what a new request is about — which fleet, which file, which date — is done for you by the service on its own key, as long as you are signed in. Set this only to keep that reading on your machine, on your key. Signed out *and* unset, a scenario whose values change between requests is declined rather than replayed on stale ones. |
285
- | `BIR_DERIVE_MODEL` | Derivation model, when you set a key of your own (default `claude-haiku-4-5-20251001`). |
286
- | `BIR_MATCH_BUDGET_MS` | How long the prompt hook waits for a match (default `2500`). |
287
- | `BIR_DERIVE_BUDGET_MS` | How long the first `PreToolUse` waits for parameters (default `8000`). |
288
- | `BIR_REPLAY_BUDGET_MS` / `BIR_STEP_TIMEOUT_MS` | Whole-plan and per-step ceilings for direct execution (default `120000` / `60000`). |
289
-
290
- ### The audit log
291
-
292
- Every decision is one `key=value` line on **stderr** — never stdout, which is the
293
- host's JSON-RPC stream. A recorder started in the background (by `bir up`, `bir
294
- setup` or the SessionStart hook) writes them to
295
- `~/.baseinstrunner/logs/<project-key>.log` — `bir status` names the file. One
296
- started in a terminal writes them to that terminal, and persistence is the
297
- shell's job:
298
-
299
- ```bash
300
- bir-hooks 2>&1 | tee -a ~/.baseinstrunner/audit.log
301
- grep run.lossy ~/.baseinstrunner/audit.log
302
- ```
303
-
304
- ---
305
-
306
- ## Failure behaviour
307
-
308
- The governing rule: **a host session never fails because of BaseInstRunner.**
309
-
310
- | Failure | Behaviour |
311
- |---|---|
312
- | No recorder running at session start | the SessionStart hook starts one in the background; a session is never blocked or failed by it |
313
- | Control server unreachable at startup | buffer ≤ 5 s, then Tier 2; logged once |
314
- | Control server dies mid-session | the proxy switches to Tier 2 for the rest; run flagged lossy |
315
- | Recorder HTTP fails | send dropped and logged; never retried inline |
316
- | Upstream fails to start | `initialize` still answers; requests return an error — one broken server, not a broken session |
317
- | Upstream dies mid-call | `isError: true` with the transport message, so the model can react |
318
- | Unknown JSON-RPC method | forwarded; any rejection is relayed back |
319
- | Step queue overflow | oldest dropped, run flagged lossy |
320
- | Host sends unparseable input | `-32700` and the relay keeps going |
321
-
322
- ---
323
-
324
- ## Development
325
-
326
- ```bash
327
- npm run build # src → dist
328
- npm test # ~400 tests: transcript identity, passthrough, correlation, config, replay, setup
329
- npm run typecheck
330
- npm run test:smoke # end-to-end replay with real processes (needs build + pretest first)
331
- ```
332
-
333
- The core test is **transcript identity**: the same scripted conversation is run
334
- against a fake upstream directly and through the proxy, and every host-visible
335
- message must match. If a change breaks transparency, that test says so first.
336
-
337
- `test/smoke-replay.mjs` is the other one worth running by hand. Everything else
338
- runs in-process; this starts a stand-in BaseIn service, a real `bir-hooks`, a real
339
- `bir-proxy` wrapping a real MCP server and a real `bir-scenario`, then drives a
340
- two-step scenario through actual stdio. It is what catches the seams between
341
- processes — and it is how the "a proxy between polls is not a missing proxy" bug
342
- was found.
343
-
344
- `test/fake-upstream.ts` is a scriptable MCP server — arbitrary capabilities,
345
- prompts, resources, `list_changed` notifications, reverse-direction
346
- `elicitation/create`, delays, failures, and dying on command. Everything else
347
- builds on it.
348
-
349
- ---
350
-
351
- ## Known gaps
352
-
353
- - **`metadata.lossy` is the value known at run start.** A run that turns lossy
354
- later says so on the audit log (`run.lossy`) and in the step gap, but the
355
- metadata is not updated: the service exposes no metadata patch, and this is
356
- exactly open question 1 in the design. Fixing it properly needs a server-side
357
- decision, not a client-side workaround.
358
- - **Subagent steps are attributed, not partitioned.** `SubagentStart` /
359
- `SubagentStop` give us `agent_id`, which is prefixed onto a step's `context`.
360
- A real agent column is a schema change, and out of scope for v1 (D6).
361
- - **`--lazy` defers to the first host message, not to `tools/list`.** Deferring
362
- further would mean answering `initialize` before the upstream exists — that is,
363
- fabricating its capabilities, which is the thing Phase 3 exists to prevent. For
364
- a client that connects at session start (Claude Code does) the flag saves
365
- nothing; for one that connects on demand it saves the whole process. See
366
- `src/upstream/lazy-client.ts`.
367
- - **Coexistence with RRepeat is untested.** Both would record the same session.
368
- Runs carry `metadata.recorder`, so server-side dedupe is possible, but nothing
369
- here detects the other (design §13, open question 2).
1
+ # BaseInstRunnerMCP
2
+
3
+ A recording MCP **proxy**. It sits between any MCP client (Claude Code, Cursor,
4
+ Codex, an Agent SDK session) and every MCP server that client uses, executes each
5
+ call on the client's behalf, and records the run as a reusable scenario in the
6
+ BaseIn service.
7
+
8
+ The design this implements is [docs/BaseInstRunner.md](docs/BaseInstRunner.md).
9
+ This README is the operator's half: how to run it, what it records, and what it
10
+ cannot. For v2 — running a calculated scenario when a prompt means the same thing
11
+ as one you have run before — see [docs/calculatedReplay.md](docs/calculatedReplay.md)
12
+ and its runbook [docs/calculatedReplayGuide.md](docs/calculatedReplayGuide.md).
13
+ The production contract, the full environment-variable reference and the
14
+ operations runbook are in [production.md](production.md).
15
+
16
+ ```
17
+ model ──mcp__chrome-devtools__*──▶ bir-proxy ──stdio/http──▶ chrome-devtools-mcp
18
+ hooks ──built-ins, prompt, stop──▶ bir-hooks (control server) ──▶ BaseIn /recordings/*
19
+ ▲
20
+ bir-proxy ────┘ step reports
21
+ ```
22
+
23
+ ---
24
+
25
+ ## Install
26
+
27
+ One line, pasted into a terminal **opened in the project you start Claude Code
28
+ in**. The console's *Set up the runner* page shows it with a one-time setup
29
+ token filled in, so there is no sign-in step either:
30
+
31
+ ```powershell
32
+ $env:BIR_SETUP_TOKEN="<token>"; irm https://api.bi2202.com/install.ps1 | iex # Windows (PowerShell)
33
+ ```
34
+
35
+ ```bash
36
+ curl -fsSL https://api.bi2202.com/install.sh | BIR_SETUP_TOKEN="<token>" sh # macOS / Linux
37
+ ```
38
+
39
+ That installs Node if the machine has none (a private copy, never on PATH),
40
+ installs this package, installs Claude Code if it is missing, and runs
41
+ `bir setup`: it signs in, stores the service address, wraps the project's MCP
42
+ servers, wires Claude Code's hooks, adds the scenario server, and starts the
43
+ recorder in the background. It ends with the one thing left to do: `claude`.
44
+ Without a token the same line signs in through the browser. Re-running it is
45
+ safe, and is also how you upgrade. To record another project: `cd` there and
46
+ run `bir setup` (no token needed; the sign-in is kept).
47
+
48
+ The scripts are [scripts/install.ps1](scripts/install.ps1) and
49
+ [scripts/install.sh](scripts/install.sh); the API serves them with its own
50
+ address filled in. Step by step, and what each line does:
51
+ [docs/quickstart.md](docs/quickstart.md). For a fleet — packaging, releases,
52
+ offline installs, updates: [docs/installRun.md](docs/installRun.md).
53
+
54
+ By hand, or when developing BaseInstRunner itself:
55
+
56
+ ```bash
57
+ npm i -g @basein/runner # or: npm install && npm run build, in a checkout
58
+ cd the-project-you-record-from
59
+ bir setup --auth-url https://api.bi2202.com # everything below, in one go
60
+ ```
61
+
62
+ or the pieces `bir setup` is made of:
63
+
64
+ ```bash
65
+ bir login # once; approve it in the browser (or --token <t> from the console)
66
+ bir install --replay # absolute paths to this package; --npx for pinned npx, --local from a checkout
67
+ bir up # the recorder, in the background; the SessionStart hook does this too
68
+ claude # your session, now recorded
69
+ ```
70
+
71
+ `bir install` leaves your shared MCP configuration as it is. Each server it wraps gets
72
+ a wrapped copy in this project's *local scope* — in `~/.claude.json`
73
+ (`$CLAUDE_CONFIG_DIR/.claude.json` when that is set), under the key Claude Code
74
+ uses for the project — which takes precedence over the `.mcp.json` or
75
+ user-scope entry of the same name, so `mcp__<server>__<tool>` names never
76
+ change (a server defined in the local scope itself is wrapped where it is).
77
+ `.mcp.json`, the file a team commits, and your user scope are never
78
+ written; wraps an older install made there are put back. `--mcp-json` (or
79
+ `BIR_WRAP_MCP_JSON=1`) is that old in-place shape, for a `.mcp.json` nobody
80
+ commits. The hooks go into `.claude/settings.local.json` — your file, not the
81
+ one a team commits; in a git repository it is added to `.git/info/exclude`.
82
+ Uninstall repairs `~/.claude.json` (and a `--config` file) entry by entry;
83
+ `settings.local.json` it puts back byte-for-byte when nothing else has edited
84
+ it since, and repairs entry by entry when something has. A project remembers
85
+ whether it was installed with `--global` (absolute paths, the default for an
86
+ installed `bir`), `--local` or `--npx`, so a later `bir install` with no flag
87
+ keeps that shape.
88
+
89
+ Some servers are left as Claude Code runs them. Their calls are still recorded
90
+ by the hooks, and `bir setup` says why for each:
91
+
92
+ - a `.mcp.json` server Claude Code has not approved yet: approve it in Claude
93
+ Code, then run `bir setup` again (`bir install --server <name>` wraps it now,
94
+ and it then starts without Claude Code's question). A rejected one is never
95
+ wrapped.
96
+ - a remote server Claude Code signs in itself (see
97
+ [What it will not do](#what-it-will-not-do)).
98
+ - every server, under a managed policy that matches servers by command or URL,
99
+ or allows only the organisation's own servers.
100
+ - every server, in a subdirectory or linked worktree of a repository: Claude
101
+ Code keeps the local scope per repository, so run `bir setup` in its root.
102
+ Linked worktrees then run the root's wrapped servers too.
103
+
104
+ A local copy remembers the entry it was made from. When that entry changes,
105
+ `bir status` marks the server STALE and `bir doctor` reports it; `bir setup`
106
+ copies it again.
107
+
108
+ **Nothing has to stay open.** The SessionStart hook is `bir-hooks ensure`, for
109
+ every install shape (an `--npx` one runs it through npx): it starts the recorder
110
+ for the project when none is running, in the background, with its audit log at
111
+ `~/.baseinstrunner/logs/<project-key>.log`, and never holds a session start
112
+ longer than about 11.5 s. `bir up` starts it by hand, `bir down` stops it,
113
+ `bir status` shows it. Running `bir-hooks` in a terminal still works and is what
114
+ the hook does for you. A recorder that dies mid-session is started again by that
115
+ session's proxies or `bir` server. `bir down` stops only the recorder, which
116
+ then stays down until the next session start, `bir up` or `bir setup`; the
117
+ hooks stay installed, so a session open there shows a "hook error" on each
118
+ prompt and tool call until it ends. To take the runner out of the way instead:
119
+ `bir off` ([When something goes wrong](#when-something-goes-wrong)).
120
+
121
+ The service address is stored by `bir setup` and `bir login` in
122
+ `~/.baseinstrunner/config.json`; `BIR_AUTH_URL` in the environment overrides
123
+ it. A cached session is bound to the service that issued it and is never sent
124
+ anywhere else.
125
+
126
+ `bir login` prints a link and a short code and waits for you to approve it in
127
+ the browser — the OAuth device grant, the same shape `gh auth login` uses. It
128
+ never handles a password, so it works for accounts that only sign in with
129
+ Google, and the link can be opened on any device, which is what makes it usable
130
+ on a headless runner. `--no-browser` prints the link without opening one;
131
+ `--token <value>` redeems a one-time setup token made on the console's *Set up
132
+ the runner* page, with no browser step at all (service 2026-09 or newer);
133
+ `--password` is the deprecated email-and-password prompt, removed next release.
134
+ The link is opened only when it is `https` (`http` only for an `http` service),
135
+ and on Windows without `cmd.exe`. `bir logout` removes this machine's session
136
+ first, then asks the service that issued it to revoke it (best effort, at most
137
+ 8 s); running recorders stop sending within seconds, and start again by
138
+ themselves after the next `bir login`.
139
+
140
+ Check it:
141
+
142
+ ```bash
143
+ bir status # what is wrapped, which config scope won, whether the runner is on
144
+ bir doctor # is it working *right now* — exits 1 only for what breaks a session here
145
+ ```
146
+
147
+ `bir doctor` sorts what it finds into problems — a server, hook or recorder a
148
+ Claude Code session here would fail on: exit 1 — and warnings, which sessions
149
+ work through (not recording, a proxy not registered yet, the service
150
+ unreachable): exit 0, or 1 with `--strict`. `--quick` does not call the service.
151
+ `bir status` also shows the recorder's view of the service, the off switch, a
152
+ port the recorder could not take, a STALE local copy, and the other projects set
153
+ up on this machine.
154
+
155
+ ### Any other MCP client
156
+
157
+ Nothing here knows where Cursor or Codex keep their settings, and it does not
158
+ guess. Two paths:
159
+
160
+ ```bash
161
+ bir install --config /path/to/that/client/mcp.json
162
+ bir wrap --server-name my-server -- node my-server.js # prints an entry to paste
163
+ ```
164
+
165
+ ### Taking it out
166
+
167
+ ```bash
168
+ bir uninstall # this project: its wrapped copies, hooks and scenario server; stops its recorder
169
+ bir uninstall --all # every project on this machine — run it before removing the package
170
+ ```
171
+
172
+ `bir uninstall` touches only this project, and names the other projects still
173
+ set up. It stops the project's recorder or, while a session is still using it,
174
+ leaves it running switched off for this project: restart that session, then
175
+ `bir down` (`--keep-recorder` leaves the recorder alone). While anything here
176
+ still runs through the runner it exits 1 with `Not everything could be undone`
177
+ and a list; fix what it names and run it again. `--server <name>` takes out only
178
+ that server, and `--replay` only the scenario server, switching replay off.
179
+
180
+ Remove the package (`npm rm -g @basein/runner`, or delete `~/.baseinstrunner`,
181
+ where the install line's copy lives) only after `bir uninstall --all` has
182
+ finished cleanly: otherwise every other project's wrapped servers and hooks name
183
+ files that are gone. (`bir logout` first revokes this machine's sign-in, which
184
+ deleting `~/.baseinstrunner` alone does not.) With `BIR_UNINSTALL=1` the install line runs
185
+ `bir uninstall --all` for you, downloading nothing; deleting the files is left
186
+ to you:
187
+
188
+ ```powershell
189
+ $env:BIR_UNINSTALL="1"; irm https://api.bi2202.com/install.ps1 | iex # Windows (PowerShell)
190
+ ```
191
+
192
+ ```bash
193
+ curl -fsSL https://api.bi2202.com/install.sh | BIR_UNINSTALL=1 sh # macOS / Linux
194
+ ```
195
+
196
+ Going back to an older runner (`BIR_VERSION=0.2.12`, or
197
+ `npm i -g @basein/runner@0.2.12`) needs `bir uninstall --all` first, run with
198
+ the current one.
199
+
200
+ ---
201
+
202
+ ## The two tiers
203
+
204
+ Only Claude Code has hooks, so run ownership is negotiated at proxy startup.
205
+
206
+ | Tier | When | Records |
207
+ |---|---|---|
208
+ | **1 — Bound** | a control server (`bir-hooks`) is discoverable for this cwd | built-ins **and** MCP, one ordered step stream, prompt and final answer |
209
+ | **2 — Standalone** | no control server within 10 s | MCP calls only — no prompt, no final answer, no built-in steps |
210
+ | **Passthrough** | as Tier 2, but for a user-scope entry an older install wrapped, started in a directory that was never set up and has no recorder | nothing — it only relays |
211
+
212
+ Tier 2 is not a bug; it is the honest ceiling of what an MCP proxy can observe. A
213
+ run records which tier produced it in `metadata.tier`, so nothing downstream
214
+ mistakes a partial trace for a complete one. A proxy that is not bound keeps
215
+ looking every 15 s and binds to a recorder that starts late or was restarted; it
216
+ records at most one standalone run of its own.
217
+
218
+ Steps taken while the tier is still being decided are **buffered**, not dropped —
219
+ the host may spawn MCP servers before `SessionStart` fires, and the ordering
220
+ between the two is not guaranteed.
221
+
222
+ ---
223
+
224
+ ## What it will not do
225
+
226
+ - **Non-MCP tools without the hook binary.** `Bash`, `Read`, `Edit` and friends
227
+ are not MCP traffic. An MCP server sees only MCP traffic addressed to it. That
228
+ is a structural fact, not a limitation of this implementation.
229
+ - **`claude-in-chrome`.** It is `scope: "dynamic"`, present in no config file, so
230
+ there is no entry to rewrite.
231
+ - **Wrap a remote server Claude Code signs in itself.** An `oauth` block, a
232
+ `headersHelper`, `${VAR}` headers or no credential header means Claude Code
233
+ holds the credentials, and a proxy would get none (the server would answer
234
+ `upstream requires sign-in`). Such a server is left unwrapped, its calls
235
+ recorded by the hooks, unless `bir install --server <name>` asks for it,
236
+ which is remembered. A static credential header travels in the wrapped
237
+ entry's env (`BIR_UPSTREAM_HEADERS`), not on its command line.
238
+ - **Approve your MCP calls.** Outside a matched replay, and unless you opt back
239
+ in, Claude Code asks about a wrapped MCP call exactly as it would unwrapped.
240
+ 0.2.12 approved them all; see [below](#two-things-to-know-before-you-turn-it-on).
241
+ - **Replay, unless you install it.** A first plain `bir install` records and nothing
242
+ else. Calculated replay — running a matched prompt's scenario instead of
243
+ rediscovering it — ships, but needs `bir install --replay`, which `bir setup`
244
+ runs unless given `--no-replay`. Once installed it is on; `bir replay off` or
245
+ `BIR_REPLAY=0` turns it off, for the reason in the next section.
246
+ - **Change what the model does.** Tool descriptions are never edited. The
247
+ model-visible changes are one declared schema key (below), the first-party
248
+ `bir` server's `run_scenario` (only with `--replay`), and replay's own
249
+ `[BaseInstRunner calculated replay]` notes when a prompt matches a scenario.
250
+ The `bir` server's three tools that only read a scenario (`scenario_show`,
251
+ `scenario_edits`, `investigate`) are listed only after `bir scenario editing on`,
252
+ or with `BIR_SCENARIO_TOOLS=read` in its environment: their definitions ride
253
+ along with every request. The three for changing one (`scenario_check`,
254
+ `scenario_edit`, `scenario_undo`) appear only after `bir scenario editing on`
255
+ in that project (not yet released, see below). With the runner off it lists
256
+ no tools at all.
257
+ - **Record its own `bir` calls** (next version, not yet released). A session
258
+ that fixes a plan is not a task to replay. The `bir` server's tools other
259
+ than `run_scenario`, and a `Bash` or `PowerShell` command made only of `bir`
260
+ commands, are never recorded as steps and never count as leaving a plan.
261
+ `npm test && bir investigate` is still recorded: `npm test` is real work.
262
+ - **Sandbox scenario code on this machine.** A calculated or hand-edited step
263
+ runs here, in the recorder's own process and context (Node's `vm`, not a
264
+ sandbox), next to your sign-in and your wrapped servers. It has a time limit
265
+ (`BIR_LOGIC_TIMEOUT_MS`, 1 s by default) and no memory limit.
266
+ The service will sandbox its own runs of the same code (with its next deploy);
267
+ the runner does not. The service's check of a hand edit only judges what the
268
+ code returns for the recording's values, so it cannot prove what the code
269
+ does here: read a hand edit before it is saved. See
270
+ [the design's §13.1](docs/calculatedReplay.md).
271
+
272
+ ---
273
+
274
+ ## Calculated replay
275
+
276
+ When the service recognises that a prompt means the same thing as one you have
277
+ run before, it hands back the *calculated scenario* derived from that earlier
278
+ run: an intent, a parameter schema, and one bit of logic per step that recomputes
279
+ that step's arguments. `bir` can run it instead of letting the model rediscover
280
+ the plan token by token.
281
+
282
+ The design is [docs/calculatedReplay.md](docs/calculatedReplay.md); the runbook —
283
+ record, calculate, check, enable, read the audit lines — is
284
+ [docs/calculatedReplayGuide.md](docs/calculatedReplayGuide.md).
285
+
286
+ ```bash
287
+ bir install --replay # adds the `bir` MCP server (local scope: Claude Code does not ask about it), replay on
288
+ bir up # or let the SessionStart hook start the recorder
289
+ bir replay allow fleet # the servers replay may call unattended — stored for this project
290
+ bir replay off # and back on with `bir replay on`; `bir replay status` shows it
291
+ ```
292
+
293
+ `bir setup` does the first two; `bir setup --no-replay` stores replay off, and so
294
+ does a plain `bir install` where replay was never set up. `bir install --replay`
295
+ never turns back on a `bir replay off` of yours. The switches are kept per
296
+ project in `~/.baseinstrunner/installed.json`, so a recorder the hook starts — with Claude
297
+ Code's environment, not your terminal's — keeps them. A recorder reads them when
298
+ it starts: `bir up --restart` applies a change. `BIR_REPLAY`,
299
+ `BIR_REPLAY_ALLOW_SERVERS` and `BIR_MIN_STEER_SIMILARITY` in the environment
300
+ still override them, and `bir doctor` says which source is in effect.
301
+
302
+ Two switches, not one, because of what replay is: **a scenario is a pre-approved
303
+ list of tool calls with computed arguments.** A steered step is answered
304
+ `permissionDecision: "allow"`, so a pinned `Bash` or `Edit` runs without the
305
+ prompt you would have seen — only a call that plausibly is the step, though (the
306
+ same command, the same file, the same kind of arguments); any other call runs as
307
+ written and is asked about as usual. A *directly executed* step never reaches the
308
+ permission system at all — it is dispatched to a `bir-proxy` and called on a
309
+ connection that is already open — and `run_scenario`, the call that starts the
310
+ run, is answered `allow` while the recorder has announced the plan. In plan mode
311
+ nothing is announced and nothing is approved. Set `BIR_REPLAY_ALLOW_SERVERS` to
312
+ the servers you are content to have called unattended (it also covers a steered
313
+ plan's recovery), and read [the guide's §5.1](docs/calculatedReplayGuide.md)
314
+ before enabling it anywhere that matters.
315
+
316
+ What it buys: on a scenario whose every step is a wrapped MCP tool, the whole
317
+ sequence runs through connections the proxies already hold — **zero model tokens**
318
+ — and the model's only job is one tool call that reads the results. Such a plan
319
+ is announced only while a `bir` server has told the recorder it is connected,
320
+ and runs only through `run_scenario`: if the model calls anything else first,
321
+ the plan is retired and nothing runs. Mixed scenarios steer the live session
322
+ instead, pinning each step's arguments. Either way the turn reports what it
323
+ cost, so the savings ledger measures something real.
324
+
325
+ **Fixing a step.** Built 2026-09-25, not yet released: it needs the next
326
+ `@basein/runner` version and a service deploy. A calculated step is code, and
327
+ code can be wrong. `bir investigate` names such a step; `bir scenario edit`
328
+ fixes it, and the service checks the change against the recording before it
329
+ saves it. **A change is saved only if it reproduces the recording and is not a
330
+ copy — or on purpose, with `--force --note "why"`.** Only the owner can change
331
+ a scenario. `bir scenario undo` takes changes back one at a time: run it again
332
+ to go one further back. An undo is checked too, and is refused if it would
333
+ break a later step or the final answer, unless you add `--force --note "why"`.
334
+ Claude can do all of this: in its terminal, or with the `bir` server's tools
335
+ once `bir scenario editing on` was run in the project. Step by step, with real
336
+ output:
337
+ [the guide's §9.2](docs/calculatedReplayGuide.md).
338
+
339
+ ```bash
340
+ bir scenario show scn_… --step 3 # read one step (scenarioId and chainRevision first)
341
+ bir scenario check scn_… --step 3 --input-logic step3.js # try a change; nothing is saved
342
+ bir scenario edit scn_… --step 3 --input-logic step3.js --note "why" # the same check, then save
343
+ bir scenario edits scn_… # the history, newest first
344
+ bir scenario undo scn_… # take back the newest edit; again to go further back
345
+ bir scenario undo scn_… --step 3 # take back step 3's newest edit only
346
+ bir scenario calc scn_… --force --discard-edits # recalculate this plan, dropping its hand edits
347
+ bir scenario editing on # let Claude's `bir` tools change scenarios here
348
+ ```
349
+
350
+ ---
351
+
352
+ ## Two things to know before you turn it on
353
+
354
+ **1. Wrapped MCP calls are not auto-approved.** Merging the hook's view of a
355
+ call with the proxy's is exact only when an id rides through the tool's
356
+ arguments, and the mechanism for that is answering `PreToolUse` with
357
+ `permissionDecision: "allow"` — which skips the permission prompt. So by default
358
+ the recorder answers an ordinary wrapped MCP call with no decision: Claude Code
359
+ asks exactly as it would without the runner, and the two views are joined on
360
+ `(server, tool, hash(args))` instead, which is lossy only under identical
361
+ concurrent calls. By default an id is carried only where that changes nothing
362
+ you are asked: a `bypassPermissions` session, and a replay-pinned step. Never in
363
+ plan mode, and never into a server whose proxy is not running.
364
+
365
+ 0.2.12 answered every wrapped call `allow`. Under `claude -p` in a workspace
366
+ whose `permissions.allow` is not read, those calls are now denied: allow them
367
+ with `permissions.allow` in a trusted workspace or `--allowedTools`, or opt back
368
+ in to auto-approval where the recorder starts — the shell you restart it from,
369
+ and the environment Claude Code starts in, since its SessionStart hook starts
370
+ the next one:
371
+
372
+ ```bash
373
+ BIR_CORRELATION_DECISION=allow bir up --restart
374
+ ```
375
+
376
+ `BIR_CORRELATION_DECISION=ask` carries the id and keeps the prompt. Hosts that
377
+ ignore `updatedInput` on a non-allow decision then fall back to fingerprint
378
+ matching.
379
+
380
+ **2. One schema key is visible to the model.** Carrying that id needs a place in
381
+ the tool's arguments, so while a recorder may inject one — a bound proxy, or one
382
+ still looking for its recorder in a project set up for it — every wrapped tool's
383
+ input schema declares one more property, `__bir_call_id__`
384
+ (`"Reserved. Leave unset."`), on every `tools/list`, whether or not an id is
385
+ ever sent. `additionalProperties` is left as the server declared it, and the id
386
+ is stripped before the server sees a call. Nothing is declared with
387
+ `--standalone` or while the runner is off. `--no-correlation` turns it off
388
+ entirely; steps then always join on `(server, tool, hash(args))`.
389
+
390
+ ---
391
+
392
+ ## Configuration
393
+
394
+ | Variable | Effect |
395
+ |---|---|
396
+ | `BIR_AUTH_URL` | BaseIn auth-service — the **API** address (normally `https://api.<your-domain>`), not the docs or app website. Overrides the address `bir setup` / `bir login` stored in `~/.baseinstrunner/config.json`. **Neither set means nothing is recorded** — everything else still works. |
397
+ | `BIR_SETUP_TOKEN` | A one-time setup token from the console, read by `bir setup` when `--token` is not given; the install scripts pass it this way. Never inherited by the recorder. |
398
+ | `BIR_AUTH_DISABLE=1` | Skip authentication (local dev / CI). |
399
+ | `BIR_OFF=1` | The kill switch for one Claude Code window: set in the environment Claude Code starts in, that window's hooks answer nothing, its wrapped servers run as plain pipes and its `bir` server lists no tools; other windows keep recording. `bir off` switches off the whole machine, `bir off --here` one project. |
400
+ | `BIR_CONTROL_PORT` | The recorder's control-server port. Default: the project's own, which `bir install` picks from 53411 up so no two projects share one. `0` is any free port (tests; installed hooks name a fixed one); a value that is not a port is ignored. |
401
+ | `BIR_CONTROL_URL` / `BIR_CONTROL_TOKEN` | Together, point a proxy (and the `bir` server) at a control server directly, bypassing discovery — the only reliable channel for an SDK session. Generated entries carry `BIR_CONTROL_URL` alone, which marks them hooks-wired; they bind to their directory's recorder through its discovery file. |
402
+ | `BIR_CORRELATION_DECISION` | Unset (default: no decision; fingerprint join), `allow` (every wrapped MCP call auto-approved, as 0.2.12 did) or `ask` — see above. Read by the recorder. |
403
+ | `BIR_NO_CORRELATION=1` | Never inject a call id. |
404
+ | `BIR_WRAP_MCP_JSON=1` | Same as `--mcp-json`: wrap `.mcp.json` servers in place. |
405
+ | `BIR_HOME` | Where state lives (default `~/.baseinstrunner`). |
406
+ | `BIR_CLAUDE_JSON` | Override the `~/.claude.json` location. Unset, it is `$CLAUDE_CONFIG_DIR/.claude.json` when `CLAUDE_CONFIG_DIR` is set, as in Claude Code. |
407
+ | `BIR_VERBOSE=1` | Detail lines on stderr: per message, plus the routine start-up, binding and stream lines that are otherwise left out. |
408
+
409
+ Calculated replay adds these, all read by `bir-hooks` only — the proxies are told
410
+ what to do when they register, so there is one switch and no second copy of it:
411
+
412
+ | Variable | Effect |
413
+ |---|---|
414
+ | `BIR_REPLAY` | `0` turns replay off and any other value on, over the project's stored switch (`bir replay on` / `off`; a first plain `bir install` stores off). Nothing below matters while it is off. |
415
+ | `BIR_REPLAY_ALLOW_SERVERS` | Server keys bir may call unattended: **direct** execution, and a steered plan's recovery. Unset means the list `bir replay allow` stored for the project, else every wrapped server; narrowing it is the recommendation. |
416
+ | `BIR_MIN_STEER_SIMILARITY` | Minimum match similarity to replay (default `0.92`, above the service's own `0.9` detection threshold). |
417
+ | `BIR_DERIVE_API_KEY` | **Optional.** Working out what a new request is about — which fleet, which file, which date — is done for you by the service on its own key, as long as you are signed in. Set this (an Anthropic key) only to keep that reading on your machine, on your key. Signed out *and* unset, a scenario whose values change between requests is declined rather than replayed on stale ones. |
418
+ | `ANTHROPIC_API_KEY` | Not used by the background recorder, which has Claude Code's environment and would bill your own key — unless `BIR_USE_ANTHROPIC_API_KEY=1`. A `bir-hooks` run by hand in a terminal uses that terminal's key. |
419
+ | `BIR_DERIVE_MODEL` | Derivation model, when you set a key of your own (default `claude-haiku-4-5-20251001`). |
420
+ | `BIR_MATCH_BUDGET_MS` | How long the prompt hook waits for a match (default `1500`). After a match slower than `BIR_MATCH_SLOW_MS` (default `800`), or one that timed out, prompts stop waiting for 5 min, doubling up to 30 min; a fast match ends that. |
421
+ | `BIR_DERIVE_BUDGET_MS` / `BIR_HOOK_DERIVE_BUDGET_MS` | How long a direct run, and a hook, waits for parameters (default `8000` / `4000`; a hook waits the smaller). |
422
+ | `BIR_PRETOOL_REPLAY_BUDGET_MS` | Everything replay does inside one `PreToolUse` (default `9000`, at most `10000`). |
423
+ | `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. |
424
+ | `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`). |
425
+ | `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. |
426
+
427
+ The `bir` server reads its own: `BIR_SCENARIO_TOOLS=read` lists its three reading
428
+ tools without `bir scenario editing on`, and `BIR_SCENARIO_RUN_TIMEOUT_MS` is how
429
+ long `run_scenario` waits for a run (default `290000`). Everything else — remote
430
+ upstreams, the recorder's backlog, service timeouts, the `bir` server's output
431
+ caps — is in [production.md](production.md).
432
+
433
+ ### The audit log
434
+
435
+ Every decision is one `key=value` line on **stderr** — never stdout, which is the
436
+ host's JSON-RPC stream. A recorder started in the background (by `bir up`, `bir
437
+ setup` or the SessionStart hook) writes them to
438
+ `~/.baseinstrunner/logs/<project-key>.log` — `bir status` names the file. One
439
+ started in a terminal writes them to that terminal, and persistence is the
440
+ shell's job:
441
+
442
+ ```bash
443
+ bir-hooks 2>&1 | tee -a ~/.baseinstrunner/audit.log
444
+ grep run.lossy ~/.baseinstrunner/audit.log
445
+ ```
446
+
447
+ ---
448
+
449
+ ## Failure behaviour
450
+
451
+ The governing rule: **a host session never fails because of BaseInstRunner.**
452
+
453
+ | Failure | Behaviour |
454
+ |---|---|
455
+ | 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 |
456
+ | Control server unreachable at startup | steps buffered ≤ 10 s, then Tier 2 (passthrough: nothing); the proxy keeps looking every 15 s and binds when one answers |
457
+ | 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 |
458
+ | A hook the recorder cannot answer in time | every route has a hard deadline (prompt 7 s, `PreToolUse` 12 s, the others 1–2 s) and answers `{}`; so does a hook with a stale token or a malformed body |
459
+ | 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 |
460
+ | 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 |
461
+ | 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 |
462
+ | Result over 128 MiB | the call ends with an error saying the result was too large to deliver, instead of hanging |
463
+ | Unknown JSON-RPC method | forwarded; any rejection is relayed back |
464
+ | Step queue overflow | oldest dropped, run flagged lossy |
465
+ | Host sends unparseable input | `-32700` and the relay keeps going |
466
+
467
+ ---
468
+
469
+ ## When something goes wrong
470
+
471
+ **`bir off` first.** It takes the runner out of the way at once, in every
472
+ project, and uninstalls nothing: hooks answer with nothing within about a
473
+ second, in sessions already open too, and nothing is recorded, replayed or sent;
474
+ wrapped MCP servers run as plain pipes to the real server from their next start
475
+ (a new session, or `/mcp` → reconnect), and keep recording until then; the `bir`
476
+ server lists no tools. `bir off --here` does the same for one project (from a
477
+ subdirectory, the installed project it is in), and `bir on` or `bir on --here`
478
+ undoes it. If the problem goes away, the runner was the cause; if it stays, the
479
+ runner is ruled out. A recorder older than the switch (0.2.12) ignores it:
480
+ `bir off` replaces one no session is using and names any other, which
481
+ `bir up --restart` replaces once its session ends. Not `bir down`: it leaves
482
+ the hooks, and an open session then shows a hook error on every prompt and tool
483
+ call.
484
+
485
+ **Then `bir doctor`**, in the project: what breaks a session here (exit 1), what
486
+ only keeps the runner from doing all of its job (warnings), and the fix for
487
+ each. `bir status` names the recorder's log, where every decision is one line
488
+ ([The audit log](#the-audit-log)).
489
+
490
+ **Then [production.md](production.md)**: the production contract, the full
491
+ environment-variable reference, the operations runbook and the findings ledger.
492
+
493
+ ---
494
+
495
+ ## Development
496
+
497
+ ```bash
498
+ npm run build # src → dist
499
+ npm test # ~1,000 tests: transcript identity, passthrough, correlation, config, replay, setup
500
+ npm run typecheck
501
+ npm run test:smoke # end-to-end replay with real processes (needs build + pretest first)
502
+ ```
503
+
504
+ The core test is **transcript identity**: the same scripted conversation is run
505
+ against a fake upstream directly and through the proxy, and every host-visible
506
+ message must match. If a change breaks transparency, that test says so first.
507
+
508
+ `test/smoke-replay.mjs` is the other one worth running by hand. Everything else
509
+ runs in-process; this starts a stand-in BaseIn service, a real `bir-hooks`, a real
510
+ `bir-proxy` wrapping a real MCP server and a real `bir-scenario`, then drives a
511
+ two-step scenario through actual stdio. It is what catches the seams between
512
+ processes — and it is how the "a proxy between polls is not a missing proxy" bug
513
+ was found.
514
+
515
+ `test/fake-upstream.ts` is a scriptable MCP server — arbitrary capabilities,
516
+ prompts, resources, `list_changed` notifications, reverse-direction
517
+ `elicitation/create`, delays, failures, and dying on command. Everything else
518
+ builds on it.
519
+
520
+ ---
521
+
522
+ ## Known gaps
523
+
524
+ - **`metadata.lossy` is the value known at run start.** A run that turns lossy
525
+ later says so on the audit log (`run.lossy`) and in the step gap, but the
526
+ metadata is not updated: the service exposes no metadata patch, and this is
527
+ exactly open question 1 in the design. Fixing it properly needs a server-side
528
+ decision, not a client-side workaround.
529
+ - **Subagent steps are attributed, not partitioned.** `SubagentStart` /
530
+ `SubagentStop` give us `agent_id`, which is prefixed onto a step's `context`.
531
+ A real agent column is a schema change, and out of scope for v1 (D6).
532
+ - **`--lazy` defers to the first host message, not to `tools/list`.** Deferring
533
+ further would mean answering `initialize` before the upstream exists — that is,
534
+ fabricating its capabilities, which is the thing Phase 3 exists to prevent. For
535
+ a client that connects at session start (Claude Code does) the flag saves
536
+ nothing; for one that connects on demand it saves the whole process. See
537
+ `src/upstream/lazy-client.ts`.
538
+ - **Coexistence with RRepeat is untested.** Both would record the same session.
539
+ Runs carry `metadata.recorder`, so server-side dedupe is possible, but nothing
540
+ here detects the other (design §13, open question 2).