@basein/runner 0.2.12 → 0.3.1

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 +2787 -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
@@ -11,18 +11,68 @@
11
11
  * message is a POST to one endpoint. The reply comes back as a JSON body or
12
12
  * as an SSE stream carrying it. A session id handed out in the
13
13
  * `Mcp-Session-Id` response header must be echoed on every later request.
14
- * Server-initiated messages arrive on a standing GET stream, opened lazily
15
- * after `initialize` and skipped when the server answers 405.
14
+ * Server-initiated messages arrive on a standing GET stream, opened once the
15
+ * handshake is done and skipped when the server answers 405.
16
16
  * - **legacy SSE** (`--transport sse`): a long-lived GET carries every reply
17
17
  * and announces the POST endpoint with an `endpoint` event, so the handshake
18
18
  * is inverted — you must be listening before you can send.
19
19
  *
20
+ * THE RULES THIS FILE KEEPS, because a wrapped server must behave at least as
21
+ * well as the same server connected directly:
22
+ *
23
+ * 1. **Every request the host sends is answered.** A network error, a non-2xx,
24
+ * an HTML page from an SSO or captive proxy, a body that is not the
25
+ * JSON-RPC answer, an SSE reply that ends or resets before the answer, an
26
+ * answer over the size cap, a legacy event stream that drops with requests
27
+ * in flight — each becomes a short, fixed JSON-RPC error for that id. The
28
+ * upstream's body is never echoed (it can be a login page or carry gateway
29
+ * internals), and 401/403 say "requires sign-in" rather than dumping HTML.
30
+ * 2. **Only the handshake is ordered.** `initialize` and then
31
+ * `notifications/initialized` go one after the other (the session id comes
32
+ * back on the first, and Python-SDK servers refuse traffic before the
33
+ * second). Every later POST is independent, as in the MCP SDK the host
34
+ * would otherwise use: parallel tool calls run in parallel, a
35
+ * `notifications/cancelled` is not queued behind the call it cancels, and
36
+ * the host's answer to a server-initiated request (roots, elicitation,
37
+ * sampling) is never stuck behind the very tool call waiting for it —
38
+ * which, serialised, was a deadlock.
39
+ * 3. **Legacy SSE sends wait for the endpoint.** Nothing is POSTed until the
40
+ * stream has announced where to POST (sent to the GET URL, the host's
41
+ * `initialize` got a 405 and the server showed as failed every session).
42
+ * A stream that ends — cleanly or not — is noticed, the requests riding it
43
+ * are answered, and it is re-established with backoff; the new session is
44
+ * re-initialised with the host's own `initialize` before anything else.
45
+ * 4. **Session expiry is survivable.** A 404 to a request that carried a
46
+ * session means the server forgot us (a redeploy, an idle timeout). The
47
+ * spec says start a new session; the proxy does it for the host — replays
48
+ * the host's cached `initialize` under a `bir:` id, sends `initialized`,
49
+ * and retries the request once.
50
+ * 5. **No timeout shorter than the host's.** The transport (http-transport.ts)
51
+ * bounds the connect only; how long a tool may take is Claude Code's call,
52
+ * and its cancellation reaches the server immediately (rule 2). What no
53
+ * host waits on is bounded here instead, so nothing holds a connection
54
+ * forever: the proxy's own calls (a replayed scenario step) are cancelled
55
+ * upstream when their caller gives up, and a notification or response the
56
+ * server has not answered in 60 s is dropped.
57
+ *
20
58
  * AUTH (§9). The host's own OAuth authenticates the server it is *configured*
21
59
  * with, which is now the proxy — so a remote upstream's credentials have to
22
60
  * reach us some other way. They come from `--header` / the environment at spawn
23
- * time and are held in memory only; nothing is written to disk here.
61
+ * time and are held in memory only; nothing is written to disk here. They are
62
+ * sent only to the configured origin: a legacy `endpoint` elsewhere is refused
63
+ * (as the MCP SDK does), a redirect off-origin drops every one of them, and a
64
+ * redirect from https to plain http is refused (http-transport.ts).
65
+ *
66
+ * STDERR IS FOR NEWS. Claude Code files every stderr line of bir-proxy as a
67
+ * "Server stderr" error, so what a remote server does routinely — a load
68
+ * balancer cutting an idle event stream every minute or two (60 s on an AWS
69
+ * ALB, 100 s on Cloudflare), and the reconnect and re-initialise that follow —
70
+ * is `BIR_VERBOSE` detail. What stays on stderr: requests answered with an
71
+ * error because a stream was lost, the first failure of an outage and its
72
+ * recovery, a session the server forgot, and a stream that keeps dropping as
73
+ * soon as it opens (once an hour per server at most).
24
74
  */
25
- import type { JsonRpcMessage } from "../jsonrpc/types.js";
75
+ import { type JsonRpcMessage } from "../jsonrpc/types.js";
26
76
  import { BaseUpstreamClient } from "./client.js";
27
77
  import { Readable } from "node:stream";
28
78
  export interface HttpUpstreamOptions {
@@ -32,42 +82,183 @@ export interface HttpUpstreamOptions {
32
82
  /** Extra headers (auth, tenancy) sent on every request. */
33
83
  headers?: Record<string, string>;
34
84
  label?: string;
35
- /** How long to wait for the SSE `endpoint` announcement. Default 15s. */
85
+ /**
86
+ * Legacy SSE: how long the whole connect may take — response headers *and*
87
+ * the `endpoint` announcement. Default 15 s. Also bounds waiting for the
88
+ * answer to a replayed `initialize`.
89
+ */
36
90
  connectTimeoutMs?: number;
91
+ /**
92
+ * Every request: DNS + TCP + TLS must finish within this. Default 10 s;
93
+ * `BIR_UPSTREAM_CONNECT_TIMEOUT_MS` overrides. Nothing bounds the answer.
94
+ */
95
+ socketConnectTimeoutMs?: number;
96
+ /** First delay before re-opening a dropped SSE stream; doubles to 30 s. Default 500 ms. */
97
+ reconnectBaseMs?: number;
98
+ /**
99
+ * Largest single answer (a JSON body, one SSE event). Default 128 MiB;
100
+ * `BIR_UPSTREAM_MAX_MESSAGE_BYTES` overrides.
101
+ */
102
+ maxMessageBytes?: number;
103
+ /** How long an idle pooled socket is kept. Default 4 s (see http-transport.ts). */
104
+ idleSocketTimeoutMs?: number;
105
+ /**
106
+ * A POST nothing waits on (a notification, the host's answer to a server
107
+ * request) is abandoned if the server has not finished answering it in this
108
+ * long. Default 60 s.
109
+ */
110
+ unansweredPostTimeoutMs?: number;
111
+ /**
112
+ * An event stream (the standing GET, the legacy stream) that stayed up this
113
+ * long was healthy: its drop restarts the backoff and is routine, not news.
114
+ * Default 60 s. Tests shorten it.
115
+ */
116
+ healthyStreamMs?: number;
37
117
  }
38
118
  export declare class HttpUpstreamClient extends BaseUpstreamClient {
39
119
  readonly ready: Promise<void>;
40
120
  private readonly url;
121
+ private readonly origin;
41
122
  private readonly transport;
42
123
  private readonly headers;
43
124
  private readonly label;
44
125
  private readonly connectTimeoutMs;
126
+ private readonly socketConnectTimeoutMs;
127
+ private readonly reconnectBaseMs;
128
+ private readonly maxMessageBytes;
129
+ private readonly unansweredPostTimeoutMs;
130
+ private readonly healthyStreamMs;
131
+ private readonly agents;
45
132
  private sessionId?;
46
- /** Legacy SSE only: where to POST, announced by the stream's `endpoint` event. */
133
+ /** Negotiated in `initialize`, echoed as `MCP-Protocol-Version` afterwards. */
134
+ private protocolVersion?;
135
+ /** The host's `initialize` params, replayed when the server loses our session. */
136
+ private initParams?;
137
+ private hostInitId?;
138
+ private reinitCount;
139
+ /**
140
+ * The handshake gate. `initialize` and `notifications/initialized` chain onto
141
+ * it; every other message waits for it and then goes on its own (rule 2).
142
+ */
143
+ private gate;
144
+ private recovery?;
145
+ private readonly outstanding;
146
+ /** Every in-flight POST's aborter, so close() can drop them all. */
147
+ private readonly inflight;
148
+ /** Answers to this client's own replayed `initialize`, consumed before the relay. */
149
+ private readonly internalWaiters;
150
+ /** Where to POST, announced by the stream's `endpoint` event. Unset while down. */
47
151
  private postUrl?;
48
- private readonly abort;
49
- /** Serialises POSTs so messages leave in the order the host sent them. */
50
- private chain;
51
- private standingStreamOpened;
152
+ /** Bumped per announced stream; a request's answer can only ride the stream it was posted on. */
153
+ private generation;
154
+ private streamAbort?;
155
+ private connecting?;
156
+ private reconnectTimer?;
157
+ private reconnectFailures;
158
+ /** Streams that ended soon after connecting, in a row: they back off like failures. */
159
+ private streamDrops;
160
+ private streamOpenedAt;
161
+ private standing;
162
+ private standingAbort?;
163
+ private standingTimer?;
164
+ private standingDrops;
165
+ private standingLastEventId?;
166
+ private standingRetryMs?;
167
+ private standingParser?;
52
168
  constructor(opts: HttpUpstreamOptions);
53
- private connect;
169
+ send(msg: JsonRpcMessage): void;
170
+ /** Never rejects: whatever goes wrong, a request still gets its answer. */
171
+ private postMessage;
172
+ private postOnce;
173
+ /** A JSON (or unexpected) reply body. Whatever it holds, the request gets an answer. */
174
+ private readReply;
175
+ /** A reply delivered as an SSE body. If it ends before the answer, answer anyway. */
176
+ private pumpReply;
177
+ /** Parse one inbound payload (a body or an SSE event). False if it was not JSON. */
178
+ private dispatchData;
179
+ /** Hand one inbound JSON-RPC message to the relay (or to a `bir:` waiter). */
180
+ private deliver;
181
+ /** Answer a request ourselves, once, unless the host no longer wants it. */
182
+ private answerError;
183
+ private noteProtocolVersion;
184
+ private handlerFailed;
185
+ /**
186
+ * The server refused the handshake for want of credentials. Said once per
187
+ * server, as `upstream.auth_required`, so `bir doctor` can explain a remote
188
+ * server that was wrapped without the headers it needs (an opt-in remote
189
+ * entry whose sign-in only the host could do) instead of "it fails".
190
+ */
191
+ private noteAuthRequired;
192
+ /**
193
+ * The server answered 404 to a request sent on session `staleKey`. Start a
194
+ * new session once, however many requests noticed at the same moment, and
195
+ * hold new traffic until it exists. Resolves true when a retry makes sense.
196
+ */
197
+ private recoverSession;
198
+ /** Which session a POST speaks for: the session id, or the legacy stream generation. */
199
+ private sessionKey;
200
+ /**
201
+ * Replay the host's `initialize` (under a `bir:` id) and `initialized`.
202
+ * `news`: say it on stderr when it worked — after a forgotten session or an
203
+ * outage; after a routine reconnect it is detail. A failure is always said.
204
+ */
205
+ private reinitialize;
206
+ /**
207
+ * Open the event stream and resolve once it has announced its endpoint. The
208
+ * whole connect — response headers included — is bounded by
209
+ * `connectTimeoutMs`: a server that never sent headers used to hold `ready`
210
+ * for fetch's 300 s, far past the host's own MCP connect timeout.
211
+ */
54
212
  private openSseEndpointStream;
55
213
  /**
56
- * Streamable HTTP: open the standing GET stream that carries server-initiated
57
- * requests (`sampling/createMessage`, `elicitation/create`, list-changed
58
- * notifications). A 405 means this server does not offer one — normal, and not
59
- * an error: everything it sends will ride the POST responses instead.
214
+ * The stream a set of requests was answered on is gone. Their answers can
215
+ * never arrive (a new stream is a new session), so answer them now.
216
+ *
217
+ * Said on stderr when that cost the host an answer. Otherwise `routine`
218
+ * (a healthy stream's idle cut, a drop a recovery already said) is detail,
219
+ * and anything else — a stream that dropped soon after it opened — is said
220
+ * once an hour per server at most.
221
+ */
222
+ private onSseStreamLost;
223
+ /** Legacy: is there an endpoint to POST to? Waits for one reconnect attempt at most. */
224
+ private ensureStream;
225
+ /**
226
+ * One reconnect attempt (shared by everyone who asks meanwhile). A host
227
+ * request that arrives while the stream is down pulls the next attempt
228
+ * forward rather than waiting out the backoff, and gets an error — not a
229
+ * hang — if that attempt fails.
230
+ *
231
+ * `afterExpiry`: the server forgot the session (a 404), which is worth a
232
+ * line when the new one is initialised. A routine reconnect that works at
233
+ * the first try is detail; one that ends an outage is said.
234
+ */
235
+ private reconnect;
236
+ private scheduleReconnect;
237
+ /**
238
+ * Open the standing GET stream that carries server-initiated requests
239
+ * (`sampling/createMessage`, `elicitation/create`, list-changed
240
+ * notifications). A 405 means this server does not offer one — normal, and
241
+ * not an error: everything it sends will ride the POST responses instead.
242
+ * When it drops it is re-opened with backoff (with `Last-Event-ID` when the
243
+ * server numbers its events), so a load balancer's idle cut does not silently
244
+ * lose every later list-changed or elicitation.
60
245
  */
61
246
  private openStandingStream;
62
- /** Read an SSE body, calling `onEvent(eventName, data)` per complete event. */
63
- private pumpSse;
64
- /** Hand one inbound JSON-RPC payload to the relay (or to a `bir:` waiter). */
65
- private dispatch;
66
- private target;
247
+ /** The standing stream is not open: anything that was promised an answer on it gets one now. */
248
+ private standingDown;
249
+ private scheduleStandingReopen;
250
+ /** A new session is starting: the old standing stream belongs to the old one. */
251
+ private resetStandingStream;
252
+ private call;
67
253
  private sessionHeaders;
68
- send(msg: JsonRpcMessage): void;
69
- private post;
254
+ /**
255
+ * A network failure with a proxy configured that this process cannot use is
256
+ * the likeliest cause; say so once in the audit log (`bir doctor` reads it).
257
+ */
258
+ private proxyHint;
70
259
  close(): void;
260
+ /** Stop every timer, stream and request; end the server-side session if there is one. */
261
+ private teardown;
71
262
  }
72
263
  /**
73
264
  * Adapt a Node stream of newline-delimited JSON into the message callback shape,