@basein/runner 0.1.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 (100) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +276 -0
  3. package/dist/auth/client.d.ts +85 -0
  4. package/dist/auth/client.js +284 -0
  5. package/dist/bin/bir-hooks.d.ts +48 -0
  6. package/dist/bin/bir-hooks.js +201 -0
  7. package/dist/bin/bir-proxy.d.ts +45 -0
  8. package/dist/bin/bir-proxy.js +207 -0
  9. package/dist/bin/bir-scenario.d.ts +24 -0
  10. package/dist/bin/bir-scenario.js +177 -0
  11. package/dist/bin/bir.d.ts +21 -0
  12. package/dist/bin/bir.js +876 -0
  13. package/dist/config/adapters/claude-code.d.ts +76 -0
  14. package/dist/config/adapters/claude-code.js +181 -0
  15. package/dist/config/adapters/generic.d.ts +17 -0
  16. package/dist/config/adapters/generic.js +36 -0
  17. package/dist/config/generate.d.ts +127 -0
  18. package/dist/config/generate.js +114 -0
  19. package/dist/config/resolve.d.ts +68 -0
  20. package/dist/config/resolve.js +132 -0
  21. package/dist/control/client.d.ts +56 -0
  22. package/dist/control/client.js +86 -0
  23. package/dist/control/correlation.d.ts +86 -0
  24. package/dist/control/correlation.js +0 -0
  25. package/dist/control/discovery.d.ts +50 -0
  26. package/dist/control/discovery.js +123 -0
  27. package/dist/control/ordering.d.ts +38 -0
  28. package/dist/control/ordering.js +44 -0
  29. package/dist/control/paths.d.ts +32 -0
  30. package/dist/control/paths.js +56 -0
  31. package/dist/control/server.d.ts +272 -0
  32. package/dist/control/server.js +1131 -0
  33. package/dist/control/transcript.d.ts +75 -0
  34. package/dist/control/transcript.js +241 -0
  35. package/dist/index.d.ts +37 -0
  36. package/dist/index.js +32 -0
  37. package/dist/jsonrpc/framing.d.ts +49 -0
  38. package/dist/jsonrpc/framing.js +143 -0
  39. package/dist/jsonrpc/types.d.ts +52 -0
  40. package/dist/jsonrpc/types.js +46 -0
  41. package/dist/proxy/intercept.d.ts +55 -0
  42. package/dist/proxy/intercept.js +147 -0
  43. package/dist/proxy/relay.d.ts +97 -0
  44. package/dist/proxy/relay.js +166 -0
  45. package/dist/proxy/session.d.ts +116 -0
  46. package/dist/proxy/session.js +319 -0
  47. package/dist/record/housekeeping.d.ts +34 -0
  48. package/dist/record/housekeeping.js +39 -0
  49. package/dist/record/queue.d.ts +48 -0
  50. package/dist/record/queue.js +96 -0
  51. package/dist/record/recorder.d.ts +111 -0
  52. package/dist/record/recorder.js +39 -0
  53. package/dist/record/redact.d.ts +37 -0
  54. package/dist/record/redact.js +119 -0
  55. package/dist/record/remote-recorder.d.ts +110 -0
  56. package/dist/record/remote-recorder.js +301 -0
  57. package/dist/record/truncate.d.ts +36 -0
  58. package/dist/record/truncate.js +85 -0
  59. package/dist/replay/bundle.d.ts +36 -0
  60. package/dist/replay/bundle.js +89 -0
  61. package/dist/replay/controller.d.ts +300 -0
  62. package/dist/replay/controller.js +807 -0
  63. package/dist/replay/coverage.d.ts +41 -0
  64. package/dist/replay/coverage.js +56 -0
  65. package/dist/replay/derive.d.ts +58 -0
  66. package/dist/replay/derive.js +166 -0
  67. package/dist/replay/executor.d.ts +78 -0
  68. package/dist/replay/executor.js +233 -0
  69. package/dist/replay/logic.d.ts +31 -0
  70. package/dist/replay/logic.js +50 -0
  71. package/dist/replay/plan.d.ts +181 -0
  72. package/dist/replay/plan.js +397 -0
  73. package/dist/replay/pricing.d.ts +41 -0
  74. package/dist/replay/pricing.js +76 -0
  75. package/dist/replay/source-run.d.ts +50 -0
  76. package/dist/replay/source-run.js +98 -0
  77. package/dist/replay/tool-error.d.ts +22 -0
  78. package/dist/replay/tool-error.js +60 -0
  79. package/dist/replay/types.d.ts +116 -0
  80. package/dist/replay/types.js +35 -0
  81. package/dist/upstream/client.d.ts +78 -0
  82. package/dist/upstream/client.js +114 -0
  83. package/dist/upstream/http-client.d.ts +78 -0
  84. package/dist/upstream/http-client.js +261 -0
  85. package/dist/upstream/lazy-client.d.ts +31 -0
  86. package/dist/upstream/lazy-client.js +53 -0
  87. package/dist/upstream/stdio-client.d.ts +57 -0
  88. package/dist/upstream/stdio-client.js +203 -0
  89. package/dist/util/log.d.ts +27 -0
  90. package/dist/util/log.js +51 -0
  91. package/dist/util/version.d.ts +2 -0
  92. package/dist/util/version.js +40 -0
  93. package/docs/BaseInstRunner.md +621 -0
  94. package/docs/calculatedReplay.md +1185 -0
  95. package/docs/calculatedReplayGuide.md +448 -0
  96. package/docs/installRun.md +413 -0
  97. package/docs/mcpmark.md +752 -0
  98. package/docs/quickstart.md +201 -0
  99. package/docs/t-bench.md +394 -0
  100. package/package.json +56 -0
@@ -0,0 +1,56 @@
1
+ /**
2
+ * paths — where BaseInstRunner keeps its state on disk.
3
+ *
4
+ * One module so the discovery file, the credentials file and the install sidecar
5
+ * cannot drift apart, and so tests can redirect all three at once with
6
+ * `BIR_HOME`. Everything lives under `~/.baseinstrunner` (mode 0700), and no
7
+ * assumption is made about `$HOME` being set — Windows sets `USERPROFILE`
8
+ * instead, and `os.homedir()` already knows the difference.
9
+ */
10
+ import { homedir } from "node:os";
11
+ import { createHash } from "node:crypto";
12
+ import { join } from "node:path";
13
+ import { mkdirSync } from "node:fs";
14
+ /** Root of BaseInstRunner's state. `BIR_HOME` overrides it (tests, sandboxes). */
15
+ export function configDir() {
16
+ return process.env.BIR_HOME ?? join(homedir(), ".baseinstrunner");
17
+ }
18
+ /** Where control servers advertise themselves, one file per project cwd. */
19
+ export function controlDir() {
20
+ return join(configDir(), "control");
21
+ }
22
+ /** The sidecar holding original MCP entries so `bir uninstall` can restore them. */
23
+ export function installedPath() {
24
+ return join(configDir(), "installed.json");
25
+ }
26
+ /** Create a directory with restrictive permissions. Idempotent, never throws. */
27
+ export function ensureDir(dir, mode = 0o700) {
28
+ try {
29
+ mkdirSync(dir, { recursive: true, mode });
30
+ }
31
+ catch {
32
+ /* already there, or unwritable — callers surface the later failure */
33
+ }
34
+ }
35
+ /**
36
+ * Separator- and case-normalised form of a path.
37
+ *
38
+ * Claude Code stores project keys with forward slashes on Windows while
39
+ * `process.cwd()` returns backslashes; comparing the literal strings is the
40
+ * number one cause of "the proxy is installed but never runs" (Phase 7.1). The
41
+ * same normalisation keys the discovery file, so a proxy started from
42
+ * `C:\proj` finds the control server registered by a hook reporting `C:/proj`.
43
+ */
44
+ export function normalizePath(p) {
45
+ const slashed = p.replace(/\\/g, "/").replace(/\/+$/, "");
46
+ return process.platform === "win32" ? slashed.toLowerCase() : slashed;
47
+ }
48
+ /** The discovery key for a project directory: `sha256(normalised cwd)[0..16]`. */
49
+ export function controlKey(cwd) {
50
+ return createHash("sha256").update(normalizePath(cwd)).digest("hex").slice(0, 16);
51
+ }
52
+ /** Absolute path of the discovery file for `cwd`. */
53
+ export function discoveryPath(cwd) {
54
+ return join(controlDir(), `${controlKey(cwd)}.json`);
55
+ }
56
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1,272 @@
1
+ /**
2
+ * control/server — the local control plane (Phase 5) and the Tier 1 recorder.
3
+ *
4
+ * WHY THIS EXISTS AT ALL (§3.2). Three proxies recording independently would
5
+ * produce three interleaved step streams with no shared ordering, and would
6
+ * double-record every call the hook also sees. One control server gives one
7
+ * `stepIndex` sequence, one run lifecycle, one HTTP chain to the recorder, and
8
+ * one place to dedupe.
9
+ *
10
+ * It is two things in one process, deliberately: the **Claude Code hook
11
+ * receiver** (Claude Code POSTs each lifecycle event to an HTTP hook) and the
12
+ * **control plane** the proxies report to. They share the run, so they must
13
+ * share the process.
14
+ *
15
+ * RUN BOUNDARIES follow RRepeat's: a Claude Code *session* holds many prompts,
16
+ * and each `UserPromptSubmit → Stop` turn is one run. Creation is deferred to
17
+ * the first prompt so the server's similar-prompt detection runs against the
18
+ * real prompt rather than an empty string; a proxy step that arrives before any
19
+ * prompt (an SDK session with no hooks) lazily opens a run with empty input,
20
+ * which is honest rather than lossy.
21
+ *
22
+ * SECURITY (§9): binds 127.0.0.1 only, requires the discovery file's bearer
23
+ * token on every route, and never echoes the token back. A loopback port that
24
+ * accepts unauthenticated step reports is a local exfiltration channel.
25
+ */
26
+ import { type Recorder } from "../record/recorder.js";
27
+ import { type ReplayOptions } from "../replay/controller.js";
28
+ /** The subset of the Claude Code hook payload we consume (all optional, all defensive). */
29
+ export interface HookPayload {
30
+ session_id?: string;
31
+ transcript_path?: string;
32
+ cwd?: string;
33
+ hook_event_name?: string;
34
+ prompt?: string;
35
+ tool_name?: string;
36
+ tool_input?: unknown;
37
+ tool_response?: unknown;
38
+ tool_use_id?: string;
39
+ error?: unknown;
40
+ agent_id?: string;
41
+ }
42
+ export interface ControlServerOptions {
43
+ recorder: Recorder;
44
+ /** Session working directory — keys the discovery file. */
45
+ cwd: string;
46
+ /** Preferred port; 0 (or a busy port) falls back to an ephemeral one. */
47
+ port?: number;
48
+ /** Loopback bearer token. Generated when omitted. */
49
+ token?: string;
50
+ /** Config keys of the servers a proxy wraps. Grows as proxies register. */
51
+ wrappedServers?: string[];
52
+ /** Host application, for run metadata. */
53
+ host?: {
54
+ app?: string;
55
+ version?: string;
56
+ };
57
+ /**
58
+ * What `PreToolUse` answers when it injects a correlation id.
59
+ *
60
+ * `"allow"` is what §6/Phase 6 specifies, and what makes injection reliable —
61
+ * but it also **auto-approves the call**, skipping a permission prompt the user
62
+ * might otherwise have seen. `"ask"` keeps the prompt at the cost of some hosts
63
+ * ignoring `updatedInput` on a non-allow decision (correlation then degrades to
64
+ * the fingerprint path). Set with `BIR_CORRELATION_DECISION`.
65
+ */
66
+ correlationDecision?: "allow" | "ask";
67
+ /** Disable id injection; use `(server, tool, hash(args))` matching instead. */
68
+ noCorrelation?: boolean;
69
+ /**
70
+ * Whether steps actually reach a service. Defaults to "the recorder is not a
71
+ * {@link NullRecorder}", which is right in every case so far.
72
+ *
73
+ * It is reported on `/health` because **this process is the only one that
74
+ * knows**: `bir doctor` runs in a different shell, with a different
75
+ * environment, and inferring it from `BIR_AUTH_URL` there once produced a
76
+ * confident "nothing will be recorded" on a session that was recording fine.
77
+ */
78
+ recording?: boolean;
79
+ /**
80
+ * Calculated replay (v2, docs/calculatedReplay.md). Omitted or
81
+ * `{ enabled: false }` leaves v1's behaviour exactly as it was: a match stops
82
+ * recording and nothing else happens.
83
+ */
84
+ replay?: ReplayOptions;
85
+ }
86
+ export interface ControlServerAddress {
87
+ url: string;
88
+ port: number;
89
+ token: string;
90
+ sessionId: string;
91
+ }
92
+ export declare class ControlServer {
93
+ readonly token: string;
94
+ readonly sessionId: string;
95
+ private readonly opts;
96
+ private readonly recorder;
97
+ private readonly queue;
98
+ private readonly sessions;
99
+ /** Config keys known to be wrapped: from install config plus live registrations. */
100
+ private readonly wrapped;
101
+ /** serverName → registration facts, for `bir doctor`. */
102
+ private readonly registrations;
103
+ private server?;
104
+ private address?;
105
+ private lossy;
106
+ /** Seals whose answer text is still being waited for. Awaited before any flush. */
107
+ private readonly pendingSeals;
108
+ /** Calculated replay. Inert unless `opts.replay.enabled`. */
109
+ private readonly replay;
110
+ constructor(opts: ControlServerOptions);
111
+ listen(): Promise<ControlServerAddress>;
112
+ close(): Promise<void>;
113
+ /** Facts for `bir doctor` / `GET /health`. */
114
+ health(): Record<string, unknown>;
115
+ private handle;
116
+ private send;
117
+ private readJson;
118
+ private ensureSession;
119
+ /** Open a run for `session`, or return the open one. */
120
+ private ensureRun;
121
+ private runMetadata;
122
+ /**
123
+ * A similar-prompt hit means the service created no run, so every later step
124
+ * post would 404. Stop recording it — that half is v1 and is mandatory.
125
+ *
126
+ * What replaces the recording is v2 (docs/calculatedReplay.md): the matched
127
+ * scenario is put through the gate ladder and, if every gate passes, armed as a
128
+ * plan. Memoized, because `ensureRun` is reached from five different hooks and
129
+ * arming twice would run the scenario twice.
130
+ *
131
+ * Resolves with the steering directive to inject, or undefined.
132
+ */
133
+ private watchForMatch;
134
+ /**
135
+ * Close the run's books, without waiting for the network.
136
+ *
137
+ * SEALING IS SPLIT FROM FLUSHING ON PURPOSE. `Stop` — the end of the turn, and
138
+ * the run boundary this whole design names — is a synchronous hook the session
139
+ * is waiting on, so it must not block on an HTTP flush. And `SessionEnd`, the
140
+ * event that used to be the *only* thing that finished a run, does not reliably
141
+ * arrive at all: it fires as the host process is exiting and loses the race.
142
+ * A run whose completeness depends on winning that race is a run that is
143
+ * routinely left half-open, which is what happened in practice.
144
+ *
145
+ * Idempotent: the second caller sees `finished` and does nothing.
146
+ */
147
+ private sealRun;
148
+ /**
149
+ * Report what a matched turn cost (docs/calculatedReplay.md §11).
150
+ *
151
+ * The live session's tokens come from the transcript **delta** since this run
152
+ * opened — never the whole file, which would bill run 3 for runs 1+2+3 and hand
153
+ * the ledger a baseline several times the truth (§11.4). A run with no
154
+ * watermark (one opened lazily by a proxy step, before any prompt) is reported
155
+ * `measured: false`: saying "unmeasured" is better than quietly inflating a
156
+ * baseline.
157
+ *
158
+ * Queued rather than awaited — `Stop` is a synchronous hook the session is
159
+ * waiting on, and a report is never worth a stall.
160
+ */
161
+ /**
162
+ * What this turn cost the model, from the transcript **delta** since the run
163
+ * opened — never the whole file, which would bill run 3 for runs 1+2+3
164
+ * (docs/calculatedReplay.md §11.4).
165
+ *
166
+ * No watermark means the delta is unknowable, so this reports `measured:
167
+ * false` and zero rather than a number that is confidently wrong.
168
+ */
169
+ private runCost;
170
+ private reportExecution;
171
+ /** Seal the run *and* wait for everything queued to reach the service. */
172
+ private finalizeRun;
173
+ private onSessionStart;
174
+ private onPrompt;
175
+ /**
176
+ * `PreToolUse`. Two shapes of answer:
177
+ *
178
+ * - **Wrapped MCP tool** → mint a call id and ask Claude Code to carry it in
179
+ * the tool's arguments, so the proxy's report and this hook's view join into
180
+ * one step (Phase 6). No step is recorded yet: the proxy owns the result and
181
+ * allocates the index when it reports.
182
+ * - **Anything else** (built-ins, unwrapped MCP) → allocate the pair now and
183
+ * record `tool_selected`. The hook is this step's only observer.
184
+ */
185
+ private onToolPre;
186
+ /**
187
+ * `PostToolUse` / `PostToolUseFailure`. For a built-in this closes the pair.
188
+ * For a correlated MCP call it normally does nothing — the proxy owns that
189
+ * result — but it arms a short fallback: if the proxy never reports (it died,
190
+ * or the call never reached it), the hook's own view is recorded after
191
+ * {@link PROXY_REPORT_GRACE_MS} rather than the step being lost entirely.
192
+ */
193
+ private onToolPost;
194
+ private errorText;
195
+ /**
196
+ * `SubagentStart` / `SubagentStop`. These carry `agent_id`, which is the only
197
+ * way to tell a subagent's tool calls from the main thread's — Claude Code
198
+ * interleaves them into one hook stream otherwise.
199
+ *
200
+ * v1 attributes rather than partitions: the active agent id is remembered and
201
+ * prefixed onto each step's `context`, because the recording schema has no
202
+ * agent column and inventing one server-side is out of scope (D6). The
203
+ * partition is available in the audit log for anyone who needs it sooner.
204
+ */
205
+ private onAgent;
206
+ private onStop;
207
+ private onSessionEnd;
208
+ private onProxyRegister;
209
+ /**
210
+ * `POST /proxy/poll` — the control→proxy channel (docs/calculatedReplay.md §16.2).
211
+ *
212
+ * The proxy dials out and parks; the server answers with work, or empty at the
213
+ * hold deadline so the proxy re-polls. Keeping the direction outbound is what
214
+ * avoids N new loopback listeners and N new tokens for a latency saving that
215
+ * would be invisible next to the upstream's own round trip (D12).
216
+ */
217
+ private onProxyPoll;
218
+ /** `POST /proxy/result` — the answer to one dispatched `tools/call`. */
219
+ private onProxyResult;
220
+ /**
221
+ * `POST /scenario/run` — `bir-scenario` asking for the armed plan to run.
222
+ *
223
+ * This is `direct` mode's whole execution: every step goes to the proxy that
224
+ * already owns its upstream, so the model spends nothing beyond the turn that
225
+ * reads the results. There is no hook timeout here, which is exactly why the
226
+ * derivation wait was deferred to it (§10).
227
+ */
228
+ private onScenarioRun;
229
+ /**
230
+ * `POST /scenario/replay` — `bir replay`, i.e. a scenario nobody matched.
231
+ *
232
+ * The CLI holds the credentials, so it fetches the scenario and posts it here;
233
+ * this process holds the proxies, so it runs it. Enabled independently of
234
+ * `BIR_REPLAY`: the operator typed a scenario id, which is a far more explicit
235
+ * consent than a similarity score, and the gate that flag guards is automatic
236
+ * arming rather than execution itself.
237
+ */
238
+ private onScenarioReplay;
239
+ /**
240
+ * A proxy's `ProxyStepReport`. Three joins, tried in order:
241
+ * 1. the injected `callId` (Tier 1, exact);
242
+ * 2. the fingerprint, inside a 30 s window (`--no-correlation`, lossy under
243
+ * identical concurrent calls — §6 says so, and so does this code);
244
+ * 3. nothing — record it standalone, which is what an SDK session with no
245
+ * hooks produces and is a complete MCP step in its own right.
246
+ */
247
+ private onProxyStep;
248
+ /**
249
+ * Which session a proxy's step belongs to.
250
+ *
251
+ * A proxy has no session id of its own — stdio MCP servers are never told one
252
+ * — so the control server decides. The order matters: preferring *any known*
253
+ * session over minting a new one is what stops a step that lands between
254
+ * `SessionStart` and the first prompt from opening a second, parallel run,
255
+ * which would split one session's steps across two recordings.
256
+ */
257
+ private sessionForProxyStep;
258
+ private matchByFingerprint;
259
+ /** Record the merged pair for a correlation and latch it against re-entry. */
260
+ private settle;
261
+ /** The one place a tool step pair is written. Redaction and caps live here. */
262
+ private record;
263
+ }
264
+ /**
265
+ * The port `bir install` writes into the hook URLs, and the one `bir-hooks`
266
+ * prefers. Claude Code's `settings.json` needs a *static* URL, so the control
267
+ * server cannot be purely ephemeral: it asks for this port and falls back to an
268
+ * ephemeral one only if it is taken — in which case the installed hooks point at
269
+ * a port nobody is listening on, and both `bir-hooks` and `bir doctor` say so.
270
+ */
271
+ export declare const DEFAULT_CONTROL_PORT = 53411;
272
+ //# sourceMappingURL=server.d.ts.map