@basein/runner 0.2.11 → 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 (144) hide show
  1. package/README.md +540 -350
  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 -10
  9. package/dist/bin/bir-scenario.js +704 -135
  10. package/dist/bin/bir.d.ts +14 -8
  11. package/dist/bin/bir.js +2937 -488
  12. package/dist/bin/investigate.js +41 -16
  13. package/dist/bin/scenario-edit.d.ts +43 -10
  14. package/dist/bin/scenario-edit.js +281 -80
  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 +2803 -306
  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/housekeeping.d.ts +71 -0
  72. package/dist/record/housekeeping.js +417 -0
  73. package/dist/record/recorder.d.ts +51 -3
  74. package/dist/record/recorder.js +3 -0
  75. package/dist/record/redact.d.ts +103 -9
  76. package/dist/record/redact.js +659 -30
  77. package/dist/record/remote-recorder.d.ts +327 -32
  78. package/dist/record/remote-recorder.js +1517 -187
  79. package/dist/record/switchable-recorder.d.ts +126 -0
  80. package/dist/record/switchable-recorder.js +351 -0
  81. package/dist/record/truncate.d.ts +40 -1
  82. package/dist/record/truncate.js +111 -4
  83. package/dist/replay/bash-channel.d.ts +35 -0
  84. package/dist/replay/bash-channel.js +133 -0
  85. package/dist/replay/bundle.d.ts +10 -0
  86. package/dist/replay/bundle.js +10 -0
  87. package/dist/replay/controller.d.ts +371 -20
  88. package/dist/replay/controller.js +1185 -111
  89. package/dist/replay/derive.d.ts +1 -0
  90. package/dist/replay/derive.js +81 -24
  91. package/dist/replay/executor.d.ts +51 -16
  92. package/dist/replay/executor.js +114 -41
  93. package/dist/replay/handover.d.ts +54 -1
  94. package/dist/replay/handover.js +55 -5
  95. package/dist/replay/logic.d.ts +62 -10
  96. package/dist/replay/logic.js +140 -22
  97. package/dist/replay/plan.d.ts +201 -27
  98. package/dist/replay/plan.js +581 -141
  99. package/dist/replay/safety.d.ts +72 -0
  100. package/dist/replay/safety.js +303 -0
  101. package/dist/replay/source-run.d.ts +10 -0
  102. package/dist/replay/source-run.js +80 -35
  103. package/dist/replay/targets.d.ts +15 -1
  104. package/dist/replay/targets.js +62 -4
  105. package/dist/replay/tool-error.d.ts +15 -1
  106. package/dist/replay/tool-error.js +33 -1
  107. package/dist/replay/types.d.ts +7 -4
  108. package/dist/upstream/client.d.ts +56 -3
  109. package/dist/upstream/client.js +61 -4
  110. package/dist/upstream/headers.d.ts +43 -0
  111. package/dist/upstream/headers.js +90 -0
  112. package/dist/upstream/http-client.d.ts +213 -22
  113. package/dist/upstream/http-client.js +1034 -168
  114. package/dist/upstream/http-transport.d.ts +143 -0
  115. package/dist/upstream/http-transport.js +391 -0
  116. package/dist/upstream/lazy-client.d.ts +8 -0
  117. package/dist/upstream/lazy-client.js +29 -1
  118. package/dist/upstream/sse.d.ts +78 -0
  119. package/dist/upstream/sse.js +189 -0
  120. package/dist/upstream/stdio-client.d.ts +61 -1
  121. package/dist/upstream/stdio-client.js +356 -33
  122. package/dist/util/atomic-write.d.ts +29 -0
  123. package/dist/util/atomic-write.js +96 -0
  124. package/dist/util/journal.d.ts +15 -0
  125. package/dist/util/journal.js +48 -3
  126. package/dist/util/log.d.ts +10 -0
  127. package/dist/util/log.js +28 -1
  128. package/dist/util/off-switch.d.ts +46 -0
  129. package/dist/util/off-switch.js +91 -0
  130. package/dist/util/service-health.d.ts +133 -0
  131. package/dist/util/service-health.js +217 -0
  132. package/docs/BaseInstRunner.md +694 -621
  133. package/docs/calculatedReplay.md +1625 -1244
  134. package/docs/calculatedReplayGuide.md +1212 -879
  135. package/docs/installRun.md +1013 -772
  136. package/docs/loginWeb.md +627 -607
  137. package/docs/mcpmark.md +49 -16
  138. package/docs/my-first-sample.md +569 -545
  139. package/docs/quickstart.md +384 -271
  140. package/docs/t-bench.md +406 -394
  141. package/package.json +58 -57
  142. package/production.md +1371 -0
  143. package/scripts/install.ps1 +97 -6
  144. package/scripts/install.sh +104 -7
@@ -7,21 +7,34 @@
7
7
  * model-visible edit (a non-goal) as well as a broken scenario corpus.
8
8
  *
9
9
  * Reversibility is not an afterthought (Phase 7.4): the original entry is stashed
10
- * verbatim in `~/.baseinstrunner/installed.json` before anything is rewritten, so
11
- * `bir uninstall` restores exactly what was there — including fields
10
+ * in `~/.baseinstrunner/installed.json` before anything is rewritten — verbatim
11
+ * but for its env and header VALUES, which uninstall reads back from the wrapped
12
+ * entry — so `bir uninstall` restores exactly what was there, including fields
12
13
  * BaseInstRunner never understood.
13
14
  */
14
- import { type McpServerConfig, type Scope } from "./resolve.js";
15
+ import { type McpServerConfig, type RemoteServerConfig, type Scope } from "./resolve.js";
15
16
  /** npm package name, as it appears in a generated `npx -p` invocation. */
16
17
  export declare const PACKAGE_NAME = "@basein/runner";
17
18
  /**
18
19
  * How the generated entry reaches a `bir` binary.
19
20
  *
20
- * npx `npx -y -p @basein/runner@<version> bir-proxy` — resolves through the
21
- * registry, so it survives a missing global install. Always pinned:
22
- * an unpinned spec lets two runners provisioned a week apart run
23
- * different proxy versions against the same service, with nothing in
24
- * the recording to say which.
21
+ * npx `npx --prefer-offline -y -p @basein/runner@<version> bir-proxy` —
22
+ * resolves through the registry, so it survives a missing global
23
+ * install. Always pinned: an unpinned spec lets two runners
24
+ * provisioned a week apart run different proxy versions against the
25
+ * same service, with nothing in the recording to say which. Pinned is
26
+ * also what makes `--prefer-offline` safe: an exact version in the
27
+ * cache cannot be stale, and without it a cached package still waits
28
+ * on the registry — measured at 70 s per server start offline, past
29
+ * the host's MCP connect timeout. An MCP entry is plain `npx` on
30
+ * every platform: Claude Code spawns MCP servers through cross-spawn,
31
+ * which finds `npx.cmd` on Windows and escapes every argument for
32
+ * cmd.exe itself. `cmd /c npx …` would hand the user's own arguments
33
+ * (a URL with `&`, a `^1.2` range, a `%VAR%`) to cmd.exe unescaped.
34
+ * Only the SessionStart hook, whose arguments are all ours, keeps
35
+ * `cmd /c` there ({@link npxHookInvocation}).
36
+ * Only on request (`--npx`), or when `bir` itself runs from the npx
37
+ * cache: every spawn pays ~1 s of npx start-up.
25
38
  * local an absolute path into this checkout — developing BaseInstRunner.
26
39
  * global an absolute path into the globally installed package. No registry
27
40
  * round trip, no PATH lookup, works offline. What a fleet wants.
@@ -66,21 +79,127 @@ export interface WrapOptions {
66
79
  * trust what the install did (see {@link nodeFlagsToCarry}).
67
80
  */
68
81
  nodeFlags?: string[];
82
+ /**
83
+ * The interpreter for `local` and `global` entries. Default
84
+ * `process.execPath`; `bir install` passes the stable path it chose
85
+ * (node-path.ts), the same one the SessionStart hook names.
86
+ */
87
+ node?: string;
88
+ /**
89
+ * Default `process.platform`. Kept for callers that pass one; an npx MCP
90
+ * entry is the same plain `npx` shape on every platform now (see
91
+ * {@link npxInvocation}).
92
+ */
93
+ platform?: NodeJS.Platform;
69
94
  }
95
+ /** True when an HTTP(S) proxy is configured in `env` (either spelling). */
96
+ export declare function proxyConfigured(env?: NodeJS.ProcessEnv): boolean;
97
+ /**
98
+ * True when NO_PROXY (either spelling) sends 127.0.0.1 straight to the socket.
99
+ *
100
+ * `--use-env-proxy` sends EVERY request through HTTP(S)_PROXY — the runner's own
101
+ * loopback traffic too: the hooks' recorder, a proxy registering, `bir setup`
102
+ * waiting for the recorder — unless NO_PROXY exempts the address. Measured on
103
+ * Node 24: `127.0.0.1`, `*` and a suffix such as `.0.0.1` do; `localhost`,
104
+ * `127.0.0.0/8` and `127.*` do not. Node and undici disagree on which spelling
105
+ * wins when both are set, so every one that is set must exempt it.
106
+ */
107
+ export declare function loopbackBypassesProxy(env?: NodeJS.ProcessEnv): boolean;
108
+ /** The addresses to add to NO_PROXY so the runner's loopback traffic skips the proxy. */
109
+ export declare const LOOPBACK_NO_PROXY = "127.0.0.1,localhost,::1";
110
+ /**
111
+ * What to tell a person whose proxy the runner will NOT use, or undefined. A
112
+ * proxy with a NO_PROXY that does not name 127.0.0.1 would swallow the runner's
113
+ * own loopback calls, so `--use-env-proxy` is left out rather than break every
114
+ * hook; the service is then reached directly, if at all.
115
+ */
116
+ export declare function envProxyHint(env?: NodeJS.ProcessEnv): string | undefined;
117
+ /**
118
+ * Make THIS process's loopback fetches skip the proxy when it runs with
119
+ * `--use-env-proxy` and NO_PROXY does not already exempt them: a launcher
120
+ * written behind a proxy keeps the flag after the shell's NO_PROXY changes.
121
+ * undici re-reads NO_PROXY per request, so fetch honours the change; so does
122
+ * every process started from here (a recorder `bir up` launches). Returns true
123
+ * when it changed anything. The flags carried into the entries are decided
124
+ * from the environment as it was ({@link nodeFlagsToCarry}).
125
+ */
126
+ export declare function exemptLoopbackFromProxy(env?: NodeJS.ProcessEnv, execArgv?: string[]): boolean;
70
127
  /**
71
128
  * The node flags this process was started with that everything it sets up
72
129
  * should share. The install scripts run `bir setup` with `--use-system-ca`
73
130
  * when the Node supports it; a hook, a proxy or a recorder started without it
74
131
  * would then fail the first TLS-inspected upload while the install itself
75
132
  * succeeded.
133
+ *
134
+ * `--use-env-proxy` is carried the same way, and added when HTTPS_PROXY or
135
+ * HTTP_PROXY is set and this Node knows the flag: Node's `fetch` ignores those
136
+ * variables otherwise, so behind a proxy-only network every service call waited
137
+ * out a connect timeout. A FLAG rather than `NODE_USE_ENV_PROXY=1` in the
138
+ * entries' env on purpose: `bir-proxy` hands its environment to the user's own
139
+ * MCP server, and that server's networking is not ours to change.
140
+ *
141
+ * ONLY WHEN NO_PROXY EXEMPTS 127.0.0.1 ({@link loopbackBypassesProxy}). With the
142
+ * flag, Node sends the runner's loopback calls to the proxy too, which refuses
143
+ * or swallows them: the SessionStart hook could not see its own recorder, the
144
+ * proxies could not register, and `bir setup` timed out — on a network where
145
+ * all of that worked without the flag. So without that exemption the flag is
146
+ * neither added nor carried, and {@link envProxyHint} says what to set.
147
+ */
148
+ export declare function nodeFlagsToCarry(execArgv?: string[], env?: NodeJS.ProcessEnv): string[];
149
+ /**
150
+ * A wrapped entry is always a stdio entry: the proxy is the process the host
151
+ * spawns. Fields the client understands and the proxy does not care about
152
+ * (`disabled`, `autoApprove`, `timeout`, …) are carried over from the original.
76
153
  */
77
- export declare function nodeFlagsToCarry(execArgv?: string[]): string[];
78
- /** A wrapped entry is always a stdio entry: the proxy is the process the host spawns. */
79
154
  export interface WrappedEntry {
80
155
  command: string;
81
156
  args: string[];
82
157
  env?: Record<string, string>;
158
+ [field: string]: unknown;
83
159
  }
160
+ /**
161
+ * The env variable a wrapped REMOTE entry carries its upstream's static
162
+ * headers in: a JSON object, `{"Header-Name": "value", …}`. bir-proxy reads it
163
+ * (merged with any `--header K:V` arguments, which an entry an older runner
164
+ * wrote still has), and never hands it to a stdio upstream. In the env rather
165
+ * than on the command line because argv is visible to every local user
166
+ * (`ps`, /proc), and a header is usually a credential.
167
+ */
168
+ export declare const UPSTREAM_HEADERS_ENV = "BIR_UPSTREAM_HEADERS";
169
+ /**
170
+ * The headers in a wrapped entry's {@link UPSTREAM_HEADERS_ENV}, or none. Only
171
+ * string values count; anything unreadable is no headers rather than an error.
172
+ */
173
+ export declare function upstreamHeadersFromEnv(env: Record<string, string> | undefined): Record<string, string>;
174
+ /**
175
+ * `npx … <bin>`, pinned and offline-first, for an MCP server entry.
176
+ *
177
+ * Plain `npx` on every platform, Windows included. Claude Code spawns MCP
178
+ * servers through cross-spawn, which resolves `npx` to `npx.cmd` and escapes
179
+ * each argument for cmd.exe the way cmd.exe reads it. Wrapping the entry in
180
+ * `cmd /c` instead hands cmd.exe a line it parses itself — and everything after
181
+ * `--` is the user's own server invocation: `?a=1&b=2` in a URL ran `b=2` as a
182
+ * command, `|` and `>` piped or redirected the proxy's stdout, `^1.2.0` lost its
183
+ * caret and `%VAR%` was expanded. `platform` is accepted and ignored.
184
+ */
185
+ export declare function npxInvocation(bin: string, version: string | undefined, _platform?: NodeJS.Platform): {
186
+ command: string;
187
+ args: string[];
188
+ };
189
+ /**
190
+ * `npx … <bin>` for a COMMAND HOOK (the SessionStart `bir-hooks ensure`).
191
+ *
192
+ * Claude Code spawns a hook's exec form directly, without cross-spawn's
193
+ * `.cmd` resolution, so on native Windows it keeps `cmd /c` — safe there
194
+ * because every argument is ours: a pinned package spec, a bin name and
195
+ * fixed flags, never a user's text.
196
+ */
197
+ export declare function npxHookInvocation(bin: string, version: string | undefined, platform?: NodeJS.Platform): {
198
+ command: string;
199
+ args: string[];
200
+ };
201
+ /** True when an entry is spawned through npx (with or without `cmd /c`). */
202
+ export declare function isNpxEntry(config: McpServerConfig): boolean;
84
203
  /** True when this entry already points at `bir-proxy` — makes install idempotent. */
85
204
  export declare function isWrapped(config: McpServerConfig): boolean;
86
205
  /**
@@ -89,6 +208,60 @@ export declare function isWrapped(config: McpServerConfig): boolean;
89
208
  * host would have spawned it.
90
209
  */
91
210
  export declare function wrapEntry(config: McpServerConfig, opts: WrapOptions): WrappedEntry;
211
+ /**
212
+ * Rebuild the original entry from a wrapped one — for an entry wrapped by an
213
+ * older runner or on another machine (nothing stashed here), and for remote
214
+ * header values, which the sidecar does not keep. `undefined` when `wrapped`
215
+ * is not an entry `wrapEntry` could have produced.
216
+ */
217
+ export declare function unwrapEntry(wrapped: McpServerConfig): McpServerConfig | undefined;
218
+ /**
219
+ * The options `wrapEntry` was given for `wrapped`, read back from the entry:
220
+ * the interpreter, its flags and the proxy script (or the npx spec), the
221
+ * control URL and `--no-correlation`. What lets a local copy be re-generated
222
+ * over a changed source without knowing how `bir install` was run.
223
+ */
224
+ export declare function wrapOptionsOf(wrapped: McpServerConfig): Omit<WrapOptions, "serverName"> | undefined;
225
+ /** Two entries that say the same thing, whatever order or optional spelling they were written in. */
226
+ export declare function sameServerConfig(a: unknown, b: unknown): boolean;
227
+ /**
228
+ * A fingerprint of an entry (see {@link canonicalServer}), kept in an
229
+ * override's record so a later change to the entry it copied is noticed —
230
+ * without keeping the entry, whose env and headers are credentials.
231
+ */
232
+ export declare function configSha(config: unknown): string;
233
+ /**
234
+ * Whether a remote entry can be wrapped without breaking it.
235
+ *
236
+ * The proxy can only send what the entry spells out. A server Claude Code signs
237
+ * in itself — OAuth through `/mcp`, a `headersHelper` script, `${VAR}` headers
238
+ * Claude Code expands — would reach the upstream with no credentials, answer
239
+ * 401 on every call, and lose the "Authenticate" option in `/mcp` because the
240
+ * entry is now a stdio one. Those are left unwrapped: their calls are still
241
+ * recorded by the PreToolUse/PostToolUse hooks. Only an entry carrying a
242
+ * literal credential header is wrapped by default; `--server <name>` wraps
243
+ * any other one anyway.
244
+ */
245
+ export declare function remoteWrapBlocker(config: RemoteServerConfig): string | undefined;
246
+ /**
247
+ * `config` as the sidecar keeps it: a remote entry's header VALUES and a stdio
248
+ * entry's env VALUES are dropped — they are credentials more often than not
249
+ * (API keys, database URLs, bearer tokens), and a second long-lived copy of
250
+ * them in installed.json is exposure for nothing. The names stay; uninstall
251
+ * reads the values back from the wrapped entry, which carries them verbatim
252
+ * (and carries a key the user rotated since, which a stash would not).
253
+ */
254
+ export declare function redactSecrets(config: McpServerConfig): {
255
+ config: McpServerConfig;
256
+ redacted: boolean;
257
+ };
258
+ /**
259
+ * Undo {@link redactSecrets}: the stash, with every redacted value read back
260
+ * from `wrapped`. Undefined when a value cannot be found there (the wrapped
261
+ * entry was changed or removed) — restoring an entry without its credentials
262
+ * would break it, so the caller leaves the entry alone instead.
263
+ */
264
+ export declare function restoreSecrets(stash: McpServerConfig, wrapped: McpServerConfig | undefined): McpServerConfig | undefined;
92
265
  /**
93
266
  * The entry for the scenario server itself. Not a proxy: it wraps nothing and
94
267
  * spawns nothing, it just forwards one call to the control server.
@@ -99,17 +272,55 @@ export declare function scenarioEntry(opts: {
99
272
  controlUrl?: string;
100
273
  version?: string;
101
274
  nodeFlags?: string[];
275
+ node?: string;
276
+ platform?: NodeJS.Platform;
102
277
  }): WrappedEntry;
103
278
  /** True when this entry is the scenario server — makes `install --replay` idempotent. */
104
279
  export declare function isScenarioServer(config: McpServerConfig): boolean;
105
280
  export interface InstalledRecord {
106
- /** Original entry, verbatim. `null` means the entry did not exist before. */
281
+ /**
282
+ * Original entry, verbatim — except header and env values (see
283
+ * {@link InstalledRecord.secretsRedacted}). `null` means the entry did not
284
+ * exist in `scope` before: uninstall removes it rather than restoring it.
285
+ */
107
286
  original: McpServerConfig | null;
108
287
  scope: Scope;
109
288
  /** File the entry was written to, so uninstall edits the same one. */
110
289
  file: string;
111
290
  cwd: string;
112
291
  installedAt: number;
292
+ /**
293
+ * Set when the wrapped entry was ADDED to the local scope as an override of
294
+ * the same name in `source` (the project's `.mcp.json`, or the user scope).
295
+ * The source entry itself is never touched: `.mcp.json` is a file a team
296
+ * commits, and the user scope applies to every project on the machine.
297
+ */
298
+ override?: {
299
+ source: Scope;
300
+ };
301
+ /**
302
+ * An override's {@link configSha} of the entry it copied, as it was then. The
303
+ * copy is frozen and outranks that entry, so a teammate's change to
304
+ * `.mcp.json` (or a token rotated in the user scope) would otherwise never
305
+ * reach this project; `bir doctor` compares it, and `bir setup` re-copies.
306
+ */
307
+ sourceSha?: string;
308
+ /** Local scope: the `~/.claude.json` project keys the entry was written under. */
309
+ keys?: string[];
310
+ /**
311
+ * `original.headers` / `original.env` hold names only; the values are read
312
+ * back from the wrapped entry (see {@link redactSecrets}). Such a record is
313
+ * kept under a {@link REDACTED_KEY_PREFIX} key, out of an older runner's sight.
314
+ */
315
+ secretsRedacted?: boolean;
316
+ /**
317
+ * Wrapped although {@link remoteWrapBlocker} advised against it, or although
318
+ * Claude Code had not yet been told to use the `.mcp.json` server it copies
319
+ * (config/mcp-approval.ts), because the person named it with `--server`.
320
+ * Remembered, so the next plain `bir setup` (the upgrade path) keeps that
321
+ * choice instead of un-wrapping it. A server they REJECTED is never kept.
322
+ */
323
+ optedIn?: boolean;
113
324
  }
114
325
  /**
115
326
  * A file we edited, captured before the first edit.
@@ -118,11 +329,64 @@ export interface InstalledRecord {
118
329
  * still hashes to it is restored from `originalText` **byte-for-byte**; a file
119
330
  * edited since is repaired entry-by-entry instead, because clobbering somebody's
120
331
  * later edit is worse than an imperfect restore.
332
+ *
333
+ * `stale` is set once somebody else changed the file between two of our
334
+ * writes: from then on `originalText` is older than a change we never saw, and
335
+ * restoring it would delete that change even though the file hashes to our
336
+ * last write. Never kept for `~/.claude.json` at all — Claude Code rewrites it
337
+ * constantly and it holds every project's settings; it is repaired entry by
338
+ * entry, always.
121
339
  */
122
340
  export interface FileBackup {
123
341
  originalText: string | null;
124
342
  writtenSha: string;
343
+ stale?: boolean;
344
+ /**
345
+ * A `.mcp.json` whose servers carry env or header VALUES (API keys, bearer
346
+ * tokens) is not kept verbatim — that would be a second long-lived copy of
347
+ * them under ~/.baseinstrunner. Its text is kept with each value's literal
348
+ * replaced by a placeholder, and {@link FileBackup.originalText} stays null;
349
+ * uninstall reads the values back from the entries in the file (wrapped or
350
+ * not) and restores the file byte-for-byte only when the result hashes to
351
+ * `sha`, the original's. See {@link protectBackupText}. A runner older than
352
+ * this field (0.2.12) reads the backup as a file install created, and never
353
+ * writes it back whole.
354
+ */
355
+ redacted?: RedactedText;
125
356
  }
357
+ /** A file's text with its env and header values replaced by placeholders. */
358
+ export interface RedactedText {
359
+ /** The text, each secret's JSON string literal replaced by a placeholder literal. */
360
+ text: string;
361
+ /** sha256 of the original text: a restore is used only when it matches. */
362
+ sha: string;
363
+ /** Placeholder `i` is the value of `mcpServers[server][field][key]`. */
364
+ secrets: Array<{
365
+ server: string;
366
+ field: "env" | "headers";
367
+ key: string;
368
+ }>;
369
+ }
370
+ /**
371
+ * The backup of a project file's pre-install text, as installed.json keeps it
372
+ * (SEC-8): verbatim when it holds no env or header value; otherwise with every
373
+ * such value's literal replaced by a placeholder ({@link FileBackup.redacted}).
374
+ * When that cannot be done exactly — a value spelled with escapes, a text that
375
+ * does not parse, a placeholder already in the text — no text is kept at all
376
+ * and the file is repaired entry by entry on uninstall (`stale`), which loses
377
+ * the byte-for-byte restore and nothing else.
378
+ */
379
+ export declare function protectBackupText(text: string): FileBackup;
380
+ /**
381
+ * The text a backup restores: its verbatim original, or the redacted one with
382
+ * every value read back from `currentText` — the file as it is now, whose
383
+ * entries (wrapped by us, or left as they were) carry those values. Null for a
384
+ * file install created; undefined when the original cannot be rebuilt exactly
385
+ * (a value is gone, or the result does not hash to the original's).
386
+ */
387
+ export declare function backupOriginalText(backup: FileBackup, currentText: string | null): string | null | undefined;
388
+ /** True when a backup says the file did not exist before install (nothing to restore). */
389
+ export declare function backupIsCreation(backup: FileBackup): boolean;
126
390
  /**
127
391
  * The replay switches an operator set for one project, kept where a recorder
128
392
  * started by a hook can read them. They used to be environment variables read
@@ -131,8 +395,21 @@ export interface FileBackup {
131
395
  * the SessionStart hook started it with Claude Code's environment instead.
132
396
  */
133
397
  export interface ReplayPolicy {
134
- /** `bir replay off` → false. Absent means on. */
398
+ /**
399
+ * `bir replay off` → false, `bir replay on` → true. Absent means on — the
400
+ * recorder reads `enabled !== false` at start (control/recorder-main.ts), and
401
+ * `BIR_REPLAY` in its environment outranks it.
402
+ */
135
403
  enabled?: boolean;
404
+ /**
405
+ * `"install"` when `enabled: false` was written by an install rather than by
406
+ * the person: `bir setup --no-replay`, `bir uninstall --replay`, or a `bir
407
+ * install` without `--replay` in a project with no scenario server. Replay
408
+ * was not asked for there, and a direct replay would have no delivery tool.
409
+ * An install that asks for replay again turns such an off back on; one the
410
+ * person set with `bir replay off` it never does. See {@link settleReplaySwitch}.
411
+ */
412
+ disabledBy?: "install";
136
413
  /** Server keys eligible for direct execution. Absent means every wrapped one. */
137
414
  allowServers?: string[];
138
415
  /** `BIR_MIN_STEER_SIMILARITY`'s stored twin. */
@@ -157,11 +434,26 @@ export interface ProjectRecord {
157
434
  * a fleet's absolute paths into pinned-npx entries.
158
435
  */
159
436
  invocation?: Invocation;
437
+ /**
438
+ * True when {@link ProjectRecord.invocation} was asked for with a flag. A
439
+ * remembered `npx` without it is the old flag-less default, which an
440
+ * installed `bir` replaces with its absolute path.
441
+ */
442
+ invocationChosen?: boolean;
443
+ /**
444
+ * Whether an install added `.claude/settings.local.json` to the repository's
445
+ * exclude file (false: the line was already there). Uninstall removes only a
446
+ * line it added. Absent (older installs): removed, as before.
447
+ */
448
+ excludeAdded?: boolean;
160
449
  /**
161
450
  * `bir scenario editing on|off` (editSteps.md D2, in the BaseIn repository):
162
451
  * whether this project's `bir` MCP server also offers the tools that CHANGE
163
452
  * a calculated scenario — `scenario_check`, `scenario_edit`, `scenario_undo`.
164
- * Absent means off. The read tools are offered either way.
453
+ * Absent means off. The tools that only READ a scenario come with it too,
454
+ * and otherwise only with `BIR_SCENARIO_TOOLS=read` in that server's
455
+ * environment: their definitions ride along with every request of every
456
+ * session, and only somebody working on a scenario calls them.
165
457
  *
166
458
  * Per project, and off by default, because the `bir` server runs in every
167
459
  * session of every installed project — a fleet included — and a plan must
@@ -169,6 +461,20 @@ export interface ProjectRecord {
169
461
  * commands ignore it: a person in a terminal is asked before each one.
170
462
  */
171
463
  editing?: boolean;
464
+ /**
465
+ * Every `.claude.json` an install for this project wrote local-scope entries
466
+ * to. Claude Code reads `$CLAUDE_CONFIG_DIR/.claude.json` when that is set,
467
+ * so a project installed from two environments (two profiles, an IDE
468
+ * terminal) has entries in two files — and an uninstall run from either must
469
+ * find both, or the other profile's servers keep naming the runner's files
470
+ * after the runner is gone.
471
+ */
472
+ claudeJsonFiles?: string[];
473
+ /**
474
+ * The upgrade note about auto-approval (wrapped MCP calls are no longer
475
+ * approved by the runner) was shown for this project; it is said once.
476
+ */
477
+ approvalNoteShown?: boolean;
172
478
  }
173
479
  export interface InstalledSidecar {
174
480
  version: 1;
@@ -187,7 +493,10 @@ export interface InstalledSidecar {
187
493
  * gets its own port, chosen once and reused.
188
494
  */
189
495
  projects?: Record<string, ProjectRecord>;
190
- /** `<cwd>::<serverName>` → record. */
496
+ /**
497
+ * `<cwd>::<serverName>` → record; `v2|<cwd>::<serverName>` for a record
498
+ * whose stash has its secrets blanked ({@link REDACTED_KEY_PREFIX}).
499
+ */
191
500
  servers: Record<string, InstalledRecord>;
192
501
  /** Absolute path → the text that was there before we touched it. */
193
502
  files?: Record<string, FileBackup>;
@@ -195,6 +504,40 @@ export interface InstalledSidecar {
195
504
  hookFiles?: string[];
196
505
  }
197
506
  export declare function readSidecar(): InstalledSidecar;
507
+ /**
508
+ * The prefix of the key a record is kept under when its stash has its
509
+ * secrets blanked ({@link InstalledRecord.secretsRedacted}).
510
+ *
511
+ * A runner older than redaction (0.2.12) finds its records by the exact key
512
+ * `<cwd>::<name>`, or by that prefix, and writes `original` back VERBATIM: on
513
+ * a rollback (`BIR_VERSION=0.2.12`, a second `bir` earlier on PATH, `npx
514
+ * @basein/runner@0.2.12`) its `bir uninstall` would put a server back with its
515
+ * API key blanked, the key's only copy — the wrapped entry — overwritten. Under
516
+ * this prefix it finds no record, says nothing to undo for that server, and
517
+ * leaves the wrapped entry, credentials and all, alone. Records it can restore
518
+ * correctly (no env, no headers, or an `original: null`) keep the old key.
519
+ */
520
+ export declare const REDACTED_KEY_PREFIX = "v2|";
521
+ /** The key a record for `serverName` in `cwd` belongs under, redacted or not. */
522
+ export declare function recordKey(cwd: string, serverName: string, redacted?: boolean): string;
523
+ /**
524
+ * Put every record under the key its stash calls for ({@link REDACTED_KEY_PREFIX}):
525
+ * a redacted record written under the plain key (by an earlier build, or by
526
+ * code that wrote the plain key) moves out of an older runner's sight, and
527
+ * two records for one server — one per key shape, left by installs of both
528
+ * versions — become the newer one. Returns true when anything moved.
529
+ */
530
+ export declare function normaliseRecordKeys(servers: Record<string, InstalledRecord>): boolean;
531
+ /**
532
+ * Write the sidecar so no reader ever sees half of it (a recorder that read a
533
+ * torn file used to start with a random token and refuse every hook).
534
+ *
535
+ * `readSidecar` answers an empty sidecar for a file it cannot parse, so the
536
+ * next write would silently replace every project's originals, tokens and
537
+ * ports with nothing. Before that happens the unreadable file is copied aside
538
+ * as `installed.json.corrupt-<time>`, where a person (or support) can get the
539
+ * originals back.
540
+ */
198
541
  export declare function writeSidecar(sidecar: InstalledSidecar): void;
199
542
  export declare function sidecarKey(cwd: string, serverName: string): string;
200
543
  /** The lowest port a project is given when none was asked for. */
@@ -224,6 +567,14 @@ export declare function probeFreePort(port: number): Promise<boolean>;
224
567
  * kept as it is — the hooks name it — and the recorder says so if it is busy.
225
568
  */
226
569
  export declare function allocateFreeProjectPort(sidecar: InstalledSidecar, cwd: string): Promise<number>;
570
+ /**
571
+ * A new port for a project whose recorded one this machine will no longer let
572
+ * it bind (a Hyper-V/WSL/Docker exclusion that moved after a reboot, or another
573
+ * program). The lowest free one no other project holds, never `avoid`;
574
+ * `undefined` when none of the 200 is free. The caller rewrites the hooks and
575
+ * every wrapped entry's control URL — `bir setup` does, by re-running install.
576
+ */
577
+ export declare function allocateReplacementPort(sidecar: InstalledSidecar, cwd: string, avoid: number): Promise<number | undefined>;
227
578
  /** Merge `patch` into the record for `cwd`, creating it when absent. */
228
579
  export declare function setProjectRecord(sidecar: InstalledSidecar, cwd: string, patch: Partial<ProjectRecord> & Pick<ProjectRecord, "port">): ProjectRecord;
229
580
  /** The bearer token this project's hooks and control server share. */
@@ -237,4 +588,31 @@ export declare function editingEnabled(sidecar: InstalledSidecar, cwd: string):
237
588
  * inventing a record would invent a port the hooks do not use.
238
589
  */
239
590
  export declare function setEditing(sidecar: InstalledSidecar, cwd: string, on: boolean): ProjectRecord | undefined;
591
+ /**
592
+ * What an install or uninstall said about calculated replay:
593
+ *
594
+ * wanted `--replay` (and `bir setup`, which adds it by default)
595
+ * refused `bir setup --no-replay`, `bir uninstall --replay`
596
+ * unasked a `bir install` without `--replay`
597
+ */
598
+ export type ReplayIntent = "wanted" | "refused" | "unasked";
599
+ /**
600
+ * Keep this project's stored replay switch ({@link ReplayPolicy.enabled}) in
601
+ * line with what the person asked for, so the recorder never steers — or runs
602
+ * steps unattended — in a project that did not opt in. It used to default to
603
+ * on everywhere: `bir setup --no-replay` printed "replay not installed" while
604
+ * the recorder replayed matched prompts anyway, and a direct plan named a
605
+ * delivery tool the session did not have.
606
+ *
607
+ * refused → off, unless it is off already.
608
+ * unasked → off, but only while the switch was never set and no scenario
609
+ * server is here (a project that has one keeps it — a plain
610
+ * `bir install --server x` must not take replay away — and
611
+ * `bir replay on|off` is the person's to keep).
612
+ * wanted → on again, but only when an install turned it off; a
613
+ * `bir replay off` stays off.
614
+ *
615
+ * Returns what changed, or undefined. The caller writes the sidecar.
616
+ */
617
+ export declare function settleReplaySwitch(sidecar: InstalledSidecar, cwd: string, intent: ReplayIntent, scenarioServerHere: boolean): "on" | "off" | undefined;
240
618
  //# sourceMappingURL=generate.d.ts.map