@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.
- package/README.md +540 -369
- package/dist/auth/client.d.ts +65 -3
- package/dist/auth/client.js +305 -53
- package/dist/bin/bir-hooks.d.ts +72 -31
- package/dist/bin/bir-hooks.js +176 -284
- package/dist/bin/bir-proxy.d.ts +17 -2
- package/dist/bin/bir-proxy.js +222 -28
- package/dist/bin/bir-scenario.d.ts +61 -21
- package/dist/bin/bir-scenario.js +593 -122
- package/dist/bin/bir.d.ts +12 -6
- package/dist/bin/bir.js +2935 -489
- package/dist/bin/investigate.js +40 -15
- package/dist/bin/scenario-edit.d.ts +7 -1
- package/dist/bin/scenario-edit.js +43 -21
- package/dist/bin/scenario-liveness.d.ts +194 -0
- package/dist/bin/scenario-liveness.js +459 -0
- package/dist/bin/scenario-stderr.d.ts +43 -0
- package/dist/bin/scenario-stderr.js +76 -0
- package/dist/bin/service-read.d.ts +88 -0
- package/dist/bin/service-read.js +128 -0
- package/dist/bin/setup.d.ts +19 -0
- package/dist/bin/setup.js +146 -20
- package/dist/config/adapters/claude-code.d.ts +92 -14
- package/dist/config/adapters/claude-code.js +291 -101
- package/dist/config/adapters/generic.d.ts +4 -1
- package/dist/config/adapters/generic.js +13 -14
- package/dist/config/generate.d.ts +392 -14
- package/dist/config/generate.js +666 -19
- package/dist/config/mcp-approval.d.ts +76 -0
- package/dist/config/mcp-approval.js +210 -0
- package/dist/config/node-path.d.ts +42 -0
- package/dist/config/node-path.js +102 -0
- package/dist/config/overrides.d.ts +62 -0
- package/dist/config/overrides.js +136 -0
- package/dist/config/resolve.d.ts +70 -8
- package/dist/config/resolve.js +186 -15
- package/dist/control/client.d.ts +22 -0
- package/dist/control/client.js +26 -7
- package/dist/control/correlation.d.ts +23 -5
- package/dist/control/correlation.js +0 -0
- package/dist/control/daemon.d.ts +400 -18
- package/dist/control/daemon.js +1105 -102
- package/dist/control/discovery.d.ts +77 -5
- package/dist/control/discovery.js +186 -11
- package/dist/control/ensure-hook.d.ts +61 -0
- package/dist/control/ensure-hook.js +160 -8
- package/dist/control/paths.d.ts +26 -0
- package/dist/control/paths.js +34 -0
- package/dist/control/pin-guard.d.ts +83 -0
- package/dist/control/pin-guard.js +534 -0
- package/dist/control/recorder-keeper.d.ts +229 -0
- package/dist/control/recorder-keeper.js +721 -0
- package/dist/control/recorder-main.d.ts +73 -0
- package/dist/control/recorder-main.js +778 -0
- package/dist/control/server.d.ts +707 -31
- package/dist/control/server.js +2787 -302
- package/dist/control/transcript.d.ts +21 -1
- package/dist/control/transcript.js +378 -112
- package/dist/index.d.ts +5 -1
- package/dist/index.js +5 -1
- package/dist/jsonrpc/framing.d.ts +76 -12
- package/dist/jsonrpc/framing.js +216 -59
- package/dist/proxy/intercept.d.ts +49 -5
- package/dist/proxy/intercept.js +188 -59
- package/dist/proxy/relay.d.ts +16 -0
- package/dist/proxy/relay.js +77 -5
- package/dist/proxy/revive.d.ts +122 -0
- package/dist/proxy/revive.js +251 -0
- package/dist/proxy/session.d.ts +222 -14
- package/dist/proxy/session.js +708 -99
- package/dist/record/recorder.d.ts +51 -3
- package/dist/record/recorder.js +3 -0
- package/dist/record/redact.d.ts +103 -9
- package/dist/record/redact.js +659 -30
- package/dist/record/remote-recorder.d.ts +327 -32
- package/dist/record/remote-recorder.js +1517 -187
- package/dist/record/switchable-recorder.d.ts +126 -0
- package/dist/record/switchable-recorder.js +351 -0
- package/dist/record/truncate.d.ts +40 -1
- package/dist/record/truncate.js +111 -4
- package/dist/replay/bash-channel.d.ts +35 -0
- package/dist/replay/bash-channel.js +133 -0
- package/dist/replay/bundle.d.ts +10 -0
- package/dist/replay/bundle.js +10 -0
- package/dist/replay/controller.d.ts +369 -20
- package/dist/replay/controller.js +1176 -107
- package/dist/replay/derive.d.ts +1 -0
- package/dist/replay/derive.js +81 -24
- package/dist/replay/executor.d.ts +51 -16
- package/dist/replay/executor.js +114 -41
- package/dist/replay/handover.d.ts +54 -1
- package/dist/replay/handover.js +55 -5
- package/dist/replay/logic.d.ts +62 -10
- package/dist/replay/logic.js +140 -22
- package/dist/replay/plan.d.ts +201 -27
- package/dist/replay/plan.js +581 -141
- package/dist/replay/safety.d.ts +72 -0
- package/dist/replay/safety.js +303 -0
- package/dist/replay/source-run.d.ts +10 -0
- package/dist/replay/source-run.js +80 -35
- package/dist/replay/targets.d.ts +15 -1
- package/dist/replay/targets.js +62 -4
- package/dist/replay/tool-error.d.ts +15 -1
- package/dist/replay/tool-error.js +33 -1
- package/dist/replay/types.d.ts +7 -4
- package/dist/upstream/client.d.ts +56 -3
- package/dist/upstream/client.js +61 -4
- package/dist/upstream/headers.d.ts +43 -0
- package/dist/upstream/headers.js +90 -0
- package/dist/upstream/http-client.d.ts +213 -22
- package/dist/upstream/http-client.js +1034 -168
- package/dist/upstream/http-transport.d.ts +143 -0
- package/dist/upstream/http-transport.js +391 -0
- package/dist/upstream/lazy-client.d.ts +8 -0
- package/dist/upstream/lazy-client.js +29 -1
- package/dist/upstream/sse.d.ts +78 -0
- package/dist/upstream/sse.js +189 -0
- package/dist/upstream/stdio-client.d.ts +61 -1
- package/dist/upstream/stdio-client.js +356 -33
- package/dist/util/atomic-write.d.ts +29 -0
- package/dist/util/atomic-write.js +96 -0
- package/dist/util/journal.d.ts +15 -0
- package/dist/util/journal.js +48 -3
- package/dist/util/log.d.ts +10 -0
- package/dist/util/log.js +28 -1
- package/dist/util/off-switch.d.ts +46 -0
- package/dist/util/off-switch.js +91 -0
- package/dist/util/service-health.d.ts +133 -0
- package/dist/util/service-health.js +217 -0
- package/docs/BaseInstRunner.md +694 -621
- package/docs/calculatedReplay.md +1625 -1304
- package/docs/calculatedReplayGuide.md +1212 -1087
- package/docs/installRun.md +1013 -772
- package/docs/loginWeb.md +627 -607
- package/docs/mcpmark.md +49 -16
- package/docs/my-first-sample.md +569 -545
- package/docs/quickstart.md +384 -311
- package/docs/t-bench.md +406 -394
- package/package.json +58 -57
- package/production.md +1371 -0
- package/scripts/install.ps1 +97 -6
- package/scripts/install.sh +104 -7
package/docs/BaseInstRunner.md
CHANGED
|
@@ -1,621 +1,694 @@
|
|
|
1
|
-
# BaseInstRunnerMCP — Design & Implementation Plan
|
|
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
|
|
5
|
-
> each call on the client's behalf, and records the run as a reusable scenario in
|
|
6
|
-
> the BaseIn service.
|
|
7
|
-
>
|
|
8
|
-
> **Status:** design.
|
|
9
|
-
>
|
|
10
|
-
>
|
|
11
|
-
>
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
|
21
|
-
|
|
22
|
-
|
|
|
23
|
-
|
|
|
24
|
-
|
|
|
25
|
-
|
|
|
26
|
-
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
|
38
|
-
|
|
39
|
-
Tier
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
`
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
│ ├─
|
|
163
|
-
│ │ ├─
|
|
164
|
-
│ │ ├─
|
|
165
|
-
│ │ └─
|
|
166
|
-
│ ├─
|
|
167
|
-
│ │ ├─
|
|
168
|
-
│ │
|
|
169
|
-
│
|
|
170
|
-
│ │
|
|
171
|
-
│ ├─
|
|
172
|
-
│ │
|
|
173
|
-
│
|
|
174
|
-
│ │ ├─
|
|
175
|
-
│ │
|
|
176
|
-
│
|
|
177
|
-
│
|
|
178
|
-
│ │ ├─
|
|
179
|
-
│ │
|
|
180
|
-
│ │
|
|
181
|
-
│ │
|
|
182
|
-
│
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
**
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
`
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
**Acceptance:**
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
### Phase
|
|
431
|
-
|
|
432
|
-
1
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
-
|
|
472
|
-
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
```
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
`
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
`
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
`
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
1
|
+
# BaseInstRunnerMCP — Design & Implementation Plan
|
|
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
|
|
5
|
+
> each call on the client's behalf, and records the run as a reusable scenario in
|
|
6
|
+
> the BaseIn service.
|
|
7
|
+
>
|
|
8
|
+
> **Status:** the design the runner was built from. Parts of it (the tiers,
|
|
9
|
+
> correlation, the degradation matrix) have since been revised to match the
|
|
10
|
+
> implementation. The production contract — measurements, environment variables,
|
|
11
|
+
> the operations runbook — is [production.md](../production.md).
|
|
12
|
+
> **Relationship to RRepeat:** a separate solution. RRepeat records by *observing*
|
|
13
|
+
> a Claude Code session through hooks; BaseInstRunnerMCP records by *being* the
|
|
14
|
+
> execution path for MCP. They share a recorder contract and can run side by side.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 0. Decisions locked
|
|
19
|
+
|
|
20
|
+
| # | Decision | Choice | Consequence |
|
|
21
|
+
|---|---|---|---|
|
|
22
|
+
| D1 | Stack | TypeScript / Node, standalone repo | Ports `McpClient`, `McpBridge`, `RemoteRecorder`, the scope merge and the auth client from RRepeat instead of reinventing them |
|
|
23
|
+
| D2 | Scope | MCP proxy **+ companion hook binary** | The only combination that reaches non-MCP tools (`Bash`, `Read`, `Edit`) |
|
|
24
|
+
| D3 | Topology | Per-server shadow, **auto-generated** config | Preserves `mcp__<server>__<tool>` names; N proxy processes, one per upstream |
|
|
25
|
+
| D4 | v1 scope | **Record only** | Replay/divergence deferred to v2 |
|
|
26
|
+
| D5 | Run correlation | **Hook binary owns the run** | One monotonic step counter across built-in and MCP steps |
|
|
27
|
+
| D6 | Persistence | BaseIn auth-service HTTP | Reuses `/recordings/*`, tenancy and the savings pipeline |
|
|
28
|
+
| D7 | Protocol | **Full transparent passthrough** | Generic bidirectional relay, not a `tools/*` switch |
|
|
29
|
+
| D8 | Callers | **Any MCP client** | No assumptions about config paths or tool-name conventions |
|
|
30
|
+
|
|
31
|
+
### 0.1 The tension in D5 + D8, and how it is resolved
|
|
32
|
+
|
|
33
|
+
D5 says the hook binary owns run identity. D8 says support any MCP client. Only
|
|
34
|
+
Claude Code has hooks. Both cannot hold universally, so run ownership is
|
|
35
|
+
**negotiated at proxy startup**, with two tiers and a passthrough that records nothing:
|
|
36
|
+
|
|
37
|
+
| Tier | When | Run owner | What gets recorded |
|
|
38
|
+
|---|---|---|---|
|
|
39
|
+
| **Tier 1 — Bound** | A BIR control server is discoverable for this cwd (Claude Code or Agent SDK with `bir-hooks` installed) | Control server | Built-ins **and** MCP, one ordered step stream, prompt and final answer included |
|
|
40
|
+
| **Tier 2 — Standalone** | No control server found within the discovery window | The proxy itself | MCP calls only. No prompt, no final answer, no built-in steps — a partial scenario, explicitly flagged |
|
|
41
|
+
| **Passthrough** | No control server, for a Claude Code user-scope entry wrapped by `bir install`, in a directory that was never installed and has no recorder | Nobody | Nothing. The proxy relays and uploads nothing |
|
|
42
|
+
|
|
43
|
+
Passthrough exists because a user-scope server is wrapped once, in one project, but
|
|
44
|
+
Claude Code starts it in every project, and Tier 2 there would upload that project's
|
|
45
|
+
tool traffic without anyone having asked. Every other entry — one written by
|
|
46
|
+
`bir install --config`, a project- or local-scope entry, a `bir wrap` entry, one
|
|
47
|
+
without `BIR_CONTROL_URL` — falls to Tier 2 from any directory, and `--standalone`
|
|
48
|
+
forces Tier 2.
|
|
49
|
+
|
|
50
|
+
Tier 2 is not a degraded bug, it is the honest ceiling of what an MCP proxy can
|
|
51
|
+
observe. Scenarios record which tier produced them so downstream consumers never
|
|
52
|
+
mistake a partial trace for a complete one.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 1. Goals and non-goals
|
|
57
|
+
|
|
58
|
+
**Goals**
|
|
59
|
+
|
|
60
|
+
1. Execute every MCP `tools/call` the client makes, on the client's behalf, and
|
|
61
|
+
return the upstream's result **losslessly** — content blocks, `structuredContent`,
|
|
62
|
+
`isError`, `_meta` — so the client cannot tell the proxy is there.
|
|
63
|
+
2. Record each call as a step in a BaseIn run: server, tool, arguments, result,
|
|
64
|
+
duration, error.
|
|
65
|
+
3. Be transparent for everything that is not `tools/call`: prompts, resources,
|
|
66
|
+
logging, completion, progress, cancellation, and the **reverse** direction
|
|
67
|
+
(sampling, elicitation, roots, list-changed notifications).
|
|
68
|
+
4. Never be the reason a host session fails. Every failure degrades to passthrough
|
|
69
|
+
or to not-recording; none propagates.
|
|
70
|
+
5. Install itself: given a client's existing MCP configuration, produce the
|
|
71
|
+
proxied configuration automatically and reversibly.
|
|
72
|
+
|
|
73
|
+
**Non-goals for v1**
|
|
74
|
+
|
|
75
|
+
- Replay, divergence bundles, steering. (v2 — §12.)
|
|
76
|
+
- Intercepting non-MCP tools *without* the hook binary. Structurally impossible.
|
|
77
|
+
- Wrapping `claude-in-chrome`. It is `scope: "dynamic"`, present in no config file,
|
|
78
|
+
so there is no entry to rewrite.
|
|
79
|
+
- Model-side behaviour change. The proxy never edits tool descriptions to steer
|
|
80
|
+
selection.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 2. The structural constraint
|
|
85
|
+
|
|
86
|
+
This is the single most important fact in the document, and every design choice
|
|
87
|
+
below follows from it.
|
|
88
|
+
|
|
89
|
+
**An MCP server sees only MCP traffic addressed to it.** The model does not choose
|
|
90
|
+
the proxy — it emits `mcp__chrome-devtools__navigate_page`, and the client routes
|
|
91
|
+
that name to whichever process is registered under the key `chrome-devtools`. That
|
|
92
|
+
makes interception a *configuration* guarantee, not a prompting one, which is
|
|
93
|
+
strong. But its reach is exactly the set of configured MCP servers:
|
|
94
|
+
|
|
95
|
+
| Traffic | Proxy sees it? | Why |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| `mcp__<wrapped>__*` | **Yes, always** | Routed by config key |
|
|
98
|
+
| `mcp__<unwrapped>__*` | No | Client connects to that server directly |
|
|
99
|
+
| `Bash`, `Read`, `Edit`, `Grep`, `Glob`, `Task`, `WebFetch` | **No** | Not MCP. Hook binary only |
|
|
100
|
+
| `claude-in-chrome` | No | Dynamic scope, no config entry |
|
|
101
|
+
| User prompt, model reasoning, final answer | **No** | Not tool traffic. Hook binary only |
|
|
102
|
+
|
|
103
|
+
Hence D2. The proxy and the hook binary are two halves of one recorder.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## 3. Architecture
|
|
108
|
+
|
|
109
|
+
```mermaid
|
|
110
|
+
graph TB
|
|
111
|
+
subgraph host["Host application (Claude Code / Cursor / SDK)"]
|
|
112
|
+
M[model loop]
|
|
113
|
+
H["hooks (Claude Code only)"]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
subgraph bir["BaseInstRunnerMCP"]
|
|
117
|
+
C["bir-hooks<br/>control server<br/>:PORT"]
|
|
118
|
+
P1["bir-proxy<br/>server: chrome-devtools"]
|
|
119
|
+
P2["bir-proxy<br/>server: github"]
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
U1[chrome-devtools-mcp]
|
|
123
|
+
U2["github mcp (http)"]
|
|
124
|
+
R[("BaseIn auth-service<br/>/recordings/*")]
|
|
125
|
+
|
|
126
|
+
M -->|"mcp__chrome-devtools__*"| P1
|
|
127
|
+
M -->|"mcp__github__*"| P2
|
|
128
|
+
H -->|"built-in steps, prompt, stop"| C
|
|
129
|
+
P1 <-->|stdio JSON-RPC| U1
|
|
130
|
+
P2 <-->|http/sse JSON-RPC| U2
|
|
131
|
+
P1 -->|"step report + call id"| C
|
|
132
|
+
P2 -->|"step report + call id"| C
|
|
133
|
+
C -->|ordered run| R
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### 3.1 Components
|
|
137
|
+
|
|
138
|
+
| Component | Process | Responsibility |
|
|
139
|
+
|---|---|---|
|
|
140
|
+
| `bir-proxy` | one per wrapped upstream, spawned by the host | Bidirectional JSON-RPC relay. Executes `tools/call` against the upstream, reports the step, forwards everything else verbatim |
|
|
141
|
+
| `bir-hooks` | one per host session | Claude Code hook receiver **and** control server. Owns run identity and the monotonic step counter. Records built-in tool steps, prompt, final answer |
|
|
142
|
+
| `bir` | CLI | `install`, `uninstall`, `status`, `doctor`, `wrap` |
|
|
143
|
+
| Recorder | library, inside `bir-hooks` (Tier 1) or `bir-proxy` (Tier 2) | Streams steps to the BaseIn service |
|
|
144
|
+
|
|
145
|
+
### 3.2 Why a control server, rather than each proxy recording directly
|
|
146
|
+
|
|
147
|
+
Three proxies recording independently would produce three interleaved step streams
|
|
148
|
+
with no shared ordering, and would double-record every call the hook also sees. A
|
|
149
|
+
single control server gives one `stepIndex` sequence, one run lifecycle, one HTTP
|
|
150
|
+
chain to the recorder, and one place to dedupe. It is the role RRepeat's
|
|
151
|
+
`HookServer` already plays, widened to accept proxy reports.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## 4. Solution layout
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
BaseInstRunnerMCP/
|
|
159
|
+
├─ package.json # bin: bir, bir-proxy, bir-hooks
|
|
160
|
+
├─ tsconfig.json
|
|
161
|
+
├─ src/
|
|
162
|
+
│ ├─ bin/
|
|
163
|
+
│ │ ├─ bir-proxy.ts # stdio MCP proxy entry point
|
|
164
|
+
│ │ ├─ bir-hooks.ts # hook receiver + control server entry point
|
|
165
|
+
│ │ └─ bir.ts # install / status / doctor / wrap
|
|
166
|
+
│ ├─ jsonrpc/
|
|
167
|
+
│ │ ├─ framing.ts # line-delimited JSON-RPC read/write
|
|
168
|
+
│ │ └─ types.ts # Request | Response | Notification
|
|
169
|
+
│ ├─ proxy/
|
|
170
|
+
│ │ ├─ relay.ts # bidirectional relay, default-forward
|
|
171
|
+
│ │ ├─ intercept.ts # the small intercept table
|
|
172
|
+
│ │ └─ session.ts # per-proxy lifecycle, tier negotiation
|
|
173
|
+
│ ├─ upstream/
|
|
174
|
+
│ │ ├─ client.ts # UpstreamClient interface
|
|
175
|
+
│ │ ├─ stdio-client.ts # spawn + framing
|
|
176
|
+
│ │ └─ http-client.ts # streamable-http and sse
|
|
177
|
+
│ ├─ control/
|
|
178
|
+
│ │ ├─ server.ts # local HTTP control plane
|
|
179
|
+
│ │ ├─ discovery.ts # ~/.baseinstrunner/control/<key>.json
|
|
180
|
+
│ │ ├─ correlation.ts # __bir_call_id__ inject/extract/dedupe
|
|
181
|
+
│ │ └─ ordering.ts # monotonic stepIndex allocator
|
|
182
|
+
│ ├─ record/
|
|
183
|
+
│ │ ├─ recorder.ts # Recorder interface
|
|
184
|
+
│ │ ├─ remote-recorder.ts # BaseIn HTTP implementation
|
|
185
|
+
│ │ ├─ redact.ts # secret scrubbing
|
|
186
|
+
│ │ └─ truncate.ts # payload size caps
|
|
187
|
+
│ ├─ config/
|
|
188
|
+
│ │ ├─ resolve.ts # merged scope resolution per client
|
|
189
|
+
│ │ ├─ generate.ts # wrap entries → proxied config
|
|
190
|
+
│ │ └─ adapters/
|
|
191
|
+
│ │ ├─ claude-code.ts # 3-scope merge, ~/.claude.json + .mcp.json
|
|
192
|
+
│ │ └─ generic.ts # explicit --config <path>
|
|
193
|
+
│ └─ auth/client.ts # login, token refresh
|
|
194
|
+
└─ test/
|
|
195
|
+
├─ fixtures/ # golden JSON-RPC transcripts
|
|
196
|
+
├─ fake-upstream.ts # scriptable MCP server for tests
|
|
197
|
+
└─ *.test.ts
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## 5. Core contracts
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
// upstream/client.ts — every upstream transport implements this.
|
|
206
|
+
export interface UpstreamClient {
|
|
207
|
+
/** Resolves when the handshake completed; rejects if the upstream is unusable. */
|
|
208
|
+
readonly ready: Promise<InitializeResult>;
|
|
209
|
+
/** Send any request and get the raw JSON-RPC result. No shape assumptions. */
|
|
210
|
+
request(method: string, params?: unknown, signal?: AbortSignal): Promise<unknown>;
|
|
211
|
+
/** Fire-and-forget notification toward the upstream. */
|
|
212
|
+
notify(method: string, params?: unknown): void;
|
|
213
|
+
/** Requests and notifications the upstream originates (sampling, elicitation, ...). */
|
|
214
|
+
onIncoming(handler: (msg: JsonRpcMessage) => void): void;
|
|
215
|
+
close(): void;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// record/recorder.ts — the surface both tiers use. Mirrors RRepeat's RemoteRecorder
|
|
219
|
+
// so the BaseIn server needs no new endpoints for v1.
|
|
220
|
+
export interface Recorder {
|
|
221
|
+
startRun(input: string, metadata?: Record<string, unknown>): string;
|
|
222
|
+
recordToolSelected(runId: string, stepIndex: number,
|
|
223
|
+
d: { toolName: string; toolInput: string; context?: string }): string;
|
|
224
|
+
recordToolResponse(runId: string, stepIndex: number,
|
|
225
|
+
d: { toolName: string; toolOutput?: string; toolError?: string }): string;
|
|
226
|
+
recordFinalAnswer(runId: string, stepIndex: number, d: { answer: string }): string;
|
|
227
|
+
finishRun(runId: string, finalOutput?: string, metrics?: RunMetrics): void;
|
|
228
|
+
flush(runId: string): Promise<void>;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// control/correlation.ts
|
|
232
|
+
export const BIR_CALL_ID = "__bir_call_id__";
|
|
233
|
+
|
|
234
|
+
// The step a proxy reports to the control server.
|
|
235
|
+
export interface ProxyStepReport {
|
|
236
|
+
callId?: string; // present in Tier 1 when the hook injected one
|
|
237
|
+
serverName: string; // config key, e.g. "chrome-devtools"
|
|
238
|
+
toolName: string; // upstream-local name, e.g. "navigate_page"
|
|
239
|
+
qualifiedName: string; // client-rendered, e.g. "mcp__chrome-devtools__navigate_page"
|
|
240
|
+
args: unknown; // post-redaction
|
|
241
|
+
result?: unknown; // whole CallToolResult, post-redaction and truncation
|
|
242
|
+
isError: boolean;
|
|
243
|
+
errorMessage?: string;
|
|
244
|
+
startedAt: number; // epoch ms
|
|
245
|
+
durationMs: number;
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
`qualifiedName` is stored **alongside** `serverName` + `toolName`, never instead of
|
|
250
|
+
them. `mcp__<server>__<tool>` is Claude Code's rendering; other clients differ.
|
|
251
|
+
Storing the parts keeps scenarios portable across hosts (D8).
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## 6. Implementation, phase by phase
|
|
256
|
+
|
|
257
|
+
Each phase ends in something runnable and testable. Do not start a phase before
|
|
258
|
+
its predecessor's acceptance test passes.
|
|
259
|
+
|
|
260
|
+
### Phase 0 — Scaffold
|
|
261
|
+
|
|
262
|
+
1. `npm init`; TypeScript strict, ESM (`"type": "module"`), Node >= 20.
|
|
263
|
+
2. `package.json` bin map:
|
|
264
|
+
```json
|
|
265
|
+
{ "bir": "dist/bin/bir.js",
|
|
266
|
+
"bir-proxy": "dist/bin/bir-proxy.js",
|
|
267
|
+
"bir-hooks": "dist/bin/bir-hooks.js" }
|
|
268
|
+
```
|
|
269
|
+
3. Zero runtime dependencies beyond `zod` if you want schema validation. Node's
|
|
270
|
+
built-in `fetch`, `child_process` and `http` cover the rest. Resist adding an MCP
|
|
271
|
+
SDK — full passthrough means handling unknown methods, which a typed SDK fights.
|
|
272
|
+
|
|
273
|
+
**Acceptance:** `npx bir --version` prints.
|
|
274
|
+
|
|
275
|
+
### Phase 1 — Framing and the bidirectional relay
|
|
276
|
+
|
|
277
|
+
This is the heart, and it is deliberately dumb.
|
|
278
|
+
|
|
279
|
+
1. `jsonrpc/framing.ts` — read newline-delimited JSON from a stream, write the same.
|
|
280
|
+
Handle partial reads, oversized lines, and invalid JSON (respond `-32700`, never
|
|
281
|
+
crash).
|
|
282
|
+
2. `proxy/relay.ts` — connect two message ports (host stdio ↔ upstream) and forward
|
|
283
|
+
every message in both directions, **by default**.
|
|
284
|
+
|
|
285
|
+
The design rule that makes full passthrough tractable:
|
|
286
|
+
|
|
287
|
+
> **Default-forward, intercept by exception.** Do not enumerate the protocol.
|
|
288
|
+
> Forward any message you have no specific reason to touch. The intercept table
|
|
289
|
+
> is three entries long.
|
|
290
|
+
|
|
291
|
+
3. Id handling. Because the relay is 1:1, host ids can pass through unchanged. Keep
|
|
292
|
+
a `Map<hostId, { method, startedAt }>` anyway — you need it to time `tools/call`
|
|
293
|
+
and to match `notifications/cancelled`. Reserve a disjoint id space (a `bir:`
|
|
294
|
+
prefix) for any request the proxy ever originates, so collisions are impossible.
|
|
295
|
+
|
|
296
|
+
**Acceptance:** `bir-proxy -- npx -y chrome-devtools-mcp@latest` behaves
|
|
297
|
+
byte-identically to running the upstream directly, verified by a golden transcript
|
|
298
|
+
in `test/fixtures/`.
|
|
299
|
+
|
|
300
|
+
### Phase 2 — Upstream transports
|
|
301
|
+
|
|
302
|
+
1. `stdio-client.ts` — `spawn(command, args, { env, stdio: ['pipe','pipe','pipe'] })`.
|
|
303
|
+
Forward upstream stderr to the proxy's own stderr **prefixed**, never to stdout;
|
|
304
|
+
a stray stdout byte corrupts the host's JSON-RPC stream. This is the single most
|
|
305
|
+
common way stdio MCP servers break.
|
|
306
|
+
2. `http-client.ts` — streamable-http and sse. Carry the entry's static headers for
|
|
307
|
+
auth, since the host's own OAuth handling does not survive a proxy in front of it
|
|
308
|
+
(§9). `bir install` writes them into the entry's env as `BIR_UPSTREAM_HEADERS`, a
|
|
309
|
+
JSON object of name to value, which the proxy reads once and removes from its
|
|
310
|
+
environment; `--header 'K: V'` is still accepted and wins on a clash. A remote
|
|
311
|
+
server Claude Code signs in itself (an `oauth` block, a `headersHelper`, `${VAR}`
|
|
312
|
+
headers, or no credential header) is left unwrapped unless `--server <name>`
|
|
313
|
+
names it.
|
|
314
|
+
3. Lifecycle: an upstream that dies for good takes the proxy with it. When a stdio
|
|
315
|
+
upstream exits or cannot be spawned, or a legacy-SSE upstream's first connect
|
|
316
|
+
fails, answer the requests in flight, then exit with the upstream's exit status
|
|
317
|
+
(1 when it has none). The host then sees what it would see unwrapped: the server
|
|
318
|
+
failed, a reconnect offered, its tools gone from the model's list. `--restart`
|
|
319
|
+
first restarts a stdio upstream once and replays the host's handshake to it.
|
|
320
|
+
`BIR_PROXY_STAY_UP=1` keeps the earlier behaviour: stay up and answer every
|
|
321
|
+
request with an error.
|
|
322
|
+
|
|
323
|
+
**Acceptance:** the Phase 1 transcript test passes against a stdio upstream, an
|
|
324
|
+
http upstream, and an upstream killed mid-call.
|
|
325
|
+
|
|
326
|
+
### Phase 3 — The intercept table
|
|
327
|
+
|
|
328
|
+
Only three methods are touched. Everything else relays untouched, both directions.
|
|
329
|
+
|
|
330
|
+
| Method | Direction | Action |
|
|
331
|
+
|---|---|---|
|
|
332
|
+
| `initialize` | host → upstream | **Relay, and rewrite as little as possible.** Forward the host's `capabilities` verbatim so the upstream learns whether sampling/elicitation exist. Return the upstream's `capabilities` and `serverInfo` verbatim, so prompts/resources/logging stay advertised. Do **not** hard-code `{ tools: {} }` — that is exactly what makes a proxy silently swallow prompts and resources. |
|
|
333
|
+
| `tools/list` | host → upstream | Relay. Where a control server may inject a call id (Phase 6), declare the `__bir_call_id__` key on each tool's input schema. Otherwise leave schemas untouched. |
|
|
334
|
+
| `tools/call` | host → upstream | Extract and strip `__bir_call_id__`; time the call; relay; report the step; return the upstream result **verbatim**. |
|
|
335
|
+
|
|
336
|
+
Everything else — `prompts/*`, `resources/*`, `completion/complete`,
|
|
337
|
+
`logging/setLevel`, `ping`, `notifications/cancelled`, `notifications/progress`,
|
|
338
|
+
and the reverse-direction `sampling/createMessage`, `elicitation/create`,
|
|
339
|
+
`roots/list`, `notifications/*/list_changed`, `notifications/message` — passes
|
|
340
|
+
through with no special case. That is the point of D7: unknown methods work
|
|
341
|
+
*because* they are never enumerated.
|
|
342
|
+
|
|
343
|
+
Two rules that are easy to get wrong:
|
|
344
|
+
|
|
345
|
+
- **Never rewrite a `tools/call` result.** Return the upstream object as-is,
|
|
346
|
+
including `_meta` and `structuredContent`. Record a *copy*.
|
|
347
|
+
- **Never let recording block the response.** Report the step on a fire-and-forget
|
|
348
|
+
queue after the result is already on its way to the host.
|
|
349
|
+
|
|
350
|
+
**Acceptance:** an upstream exposing prompts and resources is fully usable through
|
|
351
|
+
the proxy — `prompts/list` and `resources/read` reach it and return.
|
|
352
|
+
|
|
353
|
+
### Phase 4 — Recorder and auth
|
|
354
|
+
|
|
355
|
+
1. Port RRepeat's `auth/client.ts` (login, `saveCredentials`, refresh-on-401).
|
|
356
|
+
2. Port `RemoteRecorder` unchanged in shape: client-generated `run_`/`step_` uuids,
|
|
357
|
+
a per-run ordered promise chain, best-effort sends, one token refresh on 401.
|
|
358
|
+
Endpoints, already live: `POST /recordings/runs`,
|
|
359
|
+
`POST /recordings/runs/:id/steps`, `PATCH /recordings/runs/:id/steps/:stepId`,
|
|
360
|
+
`POST /recordings/runs/:id/finish`.
|
|
361
|
+
3. `redact.ts` — scrub before anything leaves the process. Minimum: values under
|
|
362
|
+
keys that name a secret, anything matching a bearer/JWT/`sk-` shape, and `env`
|
|
363
|
+
blocks. Applied to arguments *and* results. Keys are matched on whole name
|
|
364
|
+
segments (`token`, `accessToken`, `x-api-key`, `basic_auth`, `api_keys`), not
|
|
365
|
+
as the substring `/(token|secret|password|api[_-]?key|authorization|cookie)/i`
|
|
366
|
+
first planned here, which also caught `max_tokens` and `nextPageToken` and lost
|
|
367
|
+
real arguments (0.2.12's wider rule, with a bare `auth`, caught `author` too).
|
|
368
|
+
`BIR_REDACT_LEGACY_KEYS=1` applies
|
|
369
|
+
0.2.12's substring rule as well.
|
|
370
|
+
4. `truncate.ts` — cap each payload (suggest 64 KiB) with an explicit
|
|
371
|
+
`{ truncated: true, originalBytes }` marker. Base64 image blocks from browser
|
|
372
|
+
MCPs will otherwise dominate every run.
|
|
373
|
+
|
|
374
|
+
**Acceptance:** a Tier 2 run appears in the BaseIn service with correct steps and
|
|
375
|
+
no secret material, verified by a redaction fixture.
|
|
376
|
+
|
|
377
|
+
### Phase 5 — Control plane and the hook binary (Tier 1)
|
|
378
|
+
|
|
379
|
+
**Discovery.** The control server writes
|
|
380
|
+
`~/.baseinstrunner/control/<sha256(cwd).slice(0,16)>.json`:
|
|
381
|
+
|
|
382
|
+
```json
|
|
383
|
+
{ "url": "http://127.0.0.1:53411",
|
|
384
|
+
"token": "<random 32 bytes, hex>",
|
|
385
|
+
"sessionId": "...",
|
|
386
|
+
"pid": 12345,
|
|
387
|
+
"startedAt": 1756600000000 }
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
A proxy resolves it in this order: `BIR_CONTROL_URL` together with
|
|
391
|
+
`BIR_CONTROL_TOKEN` in its env (the only reliable channel for SDK sessions) → the
|
|
392
|
+
discovery file for its cwd, URL and token both → give up. The `BIR_CONTROL_URL`
|
|
393
|
+
that install writes into a generated entry is not registered against on its own:
|
|
394
|
+
for a user-scope server it names another project's recorder. Give-up is bounded:
|
|
395
|
+
**buffer steps in memory for up to 10 s while retrying**, then fall to Tier 2 and
|
|
396
|
+
flush the buffer into a self-owned run (or to passthrough, §0.1, which drops it).
|
|
397
|
+
The window exists because the host may spawn MCP servers before `SessionStart`
|
|
398
|
+
fires; the ordering between the two is not guaranteed. It is not a one-way door:
|
|
399
|
+
while unbound, the proxy looks again every 15 s and binds when a recorder answers.
|
|
400
|
+
|
|
401
|
+
**Control server HTTP surface** (loopback only, ephemeral port, bearer token from
|
|
402
|
+
the discovery file required on every route):
|
|
403
|
+
|
|
404
|
+
| Route | Caller | Purpose |
|
|
405
|
+
|---|---|---|
|
|
406
|
+
| `POST /session/start` | hook | Open the run; returns `runId` |
|
|
407
|
+
| `POST /session/prompt` | hook | `UserPromptSubmit` — the run's `input` |
|
|
408
|
+
| `POST /tool/pre` | hook | Built-in step opened; returns `{ stepIndex, callId? }` |
|
|
409
|
+
| `POST /tool/post` | hook | Built-in step closed with its result |
|
|
410
|
+
| `POST /session/stop` | hook | Final answer |
|
|
411
|
+
| `POST /session/end` | hook | `finishRun` + flush |
|
|
412
|
+
| `POST /proxy/register` | proxy | `{ serverName, pid, cwd }` → `{ runId, sessionId }` |
|
|
413
|
+
| `POST /proxy/step` | proxy | A `ProxyStepReport`; allocates `stepIndex` |
|
|
414
|
+
| `GET /health` | CLI | `bir doctor` |
|
|
415
|
+
|
|
416
|
+
**Ordering.** `ordering.ts` hands out `stepIndex` from one counter shared by hook
|
|
417
|
+
and proxy reports. Allocation happens at *report* time, not call time, so
|
|
418
|
+
concurrent MCP calls are ordered by completion — deterministic, and honest about
|
|
419
|
+
what actually happened.
|
|
420
|
+
|
|
421
|
+
**Hook wiring.** `bir-hooks` registers `UserPromptSubmit`, `PreToolUse`,
|
|
422
|
+
`PostToolUse`, `PostToolUseFailure`, `Stop`, `StopFailure`, `SessionStart`,
|
|
423
|
+
`SessionEnd`. Use `PostToolUseFailure` rather than inferring failure from
|
|
424
|
+
`PostToolUse` shape, and `SubagentStart` / `SubagentStop` (which carry `agent_id`)
|
|
425
|
+
to attribute subagent steps correctly.
|
|
426
|
+
|
|
427
|
+
**Acceptance:** a Claude Code session running one built-in and one MCP tool
|
|
428
|
+
produces a single run whose steps are in true execution order.
|
|
429
|
+
|
|
430
|
+
### Phase 6 — Correlation and dedupe
|
|
431
|
+
|
|
432
|
+
In Tier 1 a wrapped MCP call is seen **twice**: once by the hook at `PreToolUse`,
|
|
433
|
+
once by the proxy at `tools/call`. Without correlation you double-record.
|
|
434
|
+
|
|
435
|
+
**Mechanism.** At `PreToolUse` for a wrapped `mcp__*` tool, the hook may return
|
|
436
|
+
`updatedInput = { ...original, [BIR_CALL_ID]: id }`. The proxy strips that key
|
|
437
|
+
before relaying and echoes it on its step report. Where no id rides along, the
|
|
438
|
+
control server joins the proxy's report to the hook's call by fingerprint
|
|
439
|
+
(below). Either way it merges the two views into one step:
|
|
440
|
+
|
|
441
|
+
- the hook contributes identity, `tool_use_id`, ordering position, and the model's
|
|
442
|
+
original arguments;
|
|
443
|
+
- the proxy contributes the true upstream result, `isError`, `structuredContent`,
|
|
444
|
+
and real execution duration.
|
|
445
|
+
|
|
446
|
+
This is RRepeat's `RREPEAT_SENTINEL` trick generalised, so it is proven — but
|
|
447
|
+
inherit its costs knowingly:
|
|
448
|
+
|
|
449
|
+
- **An id rides only with a permission decision.** Claude Code takes
|
|
450
|
+
`updatedInput` together with `permissionDecision: "allow"`, and `allow` skips
|
|
451
|
+
the prompt the user would have seen for that call. So the hook injects an id
|
|
452
|
+
only where `allow` changes nothing the user is asked (a session in
|
|
453
|
+
`bypassPermissions` mode) or is answered anyway (a step a replay pinned, whose
|
|
454
|
+
`updatedInput` must be applied; replay is its own consent). Never in `plan`
|
|
455
|
+
mode, and never into a server with no live proxy to strip it. Every other call
|
|
456
|
+
gets no decision from the hook and joins by fingerprint.
|
|
457
|
+
`BIR_CORRELATION_DECISION=allow` in the recorder's environment injects with
|
|
458
|
+
`allow` for every wrapped call, auto-approving them all (0.2.12's behaviour);
|
|
459
|
+
`=ask` injects with `ask`, which keeps the prompt.
|
|
460
|
+
- **The key has to be declared in the tool's schema** —
|
|
461
|
+
`__bir_call_id__: { type: "string", description: "Reserved. Leave unset." }` —
|
|
462
|
+
and that declaration is visible to the model on every call, not only recorded
|
|
463
|
+
ones. `additionalProperties` stays whatever the upstream declared.
|
|
464
|
+
|
|
465
|
+
Therefore:
|
|
466
|
+
|
|
467
|
+
- Declare the key **only** where a control server may inject one: while bound,
|
|
468
|
+
or while still negotiating in a directory set up for it (a discovery file, an
|
|
469
|
+
SDK channel or an install record), so a warm and a cold start show the model
|
|
470
|
+
the same schemas. Tier 2, passthrough and `bir off` never declare it.
|
|
471
|
+
- Declare it **only** on wrapped servers.
|
|
472
|
+
- Provide `--no-correlation` on the proxy (`bir install --no-correlation` writes
|
|
473
|
+
it), which declares nothing, and `BIR_NO_CORRELATION=1` on the recorder, which
|
|
474
|
+
injects nothing. Steps then join by fingerprint —
|
|
475
|
+
`(serverName, toolName, hash(args))`, the arguments hashed as they ran, before
|
|
476
|
+
redaction, within 10 minutes of the `PreToolUse` (the user may be reading a
|
|
477
|
+
permission prompt) — as every ordinary call does by default. Document it as
|
|
478
|
+
lossy under identical concurrent calls.
|
|
479
|
+
|
|
480
|
+
**Acceptance:** one MCP call in a Claude Code session produces exactly one step,
|
|
481
|
+
carrying both the model's arguments and the upstream's real result.
|
|
482
|
+
|
|
483
|
+
### Phase 7 — Config generation and the CLI
|
|
484
|
+
|
|
485
|
+
1. `config/adapters/claude-code.ts` — merge the three scopes in Claude Code's own
|
|
486
|
+
precedence order: **Local** (`~/.claude.json` → `projects[<cwd>].mcpServers`)
|
|
487
|
+
beats **Project** (`<cwd>/.mcp.json`) beats **User** (`~/.claude.json` top-level).
|
|
488
|
+
Match the local-scope key on a *normalised* path: Claude Code stores forward
|
|
489
|
+
slashes on Windows while `process.cwd()` returns backslashes. Getting this wrong
|
|
490
|
+
is the number one cause of "the proxy is installed but never runs".
|
|
491
|
+
2. `config/generate.ts` — generate a wrapped entry for each resolved one, keeping the key:
|
|
492
|
+
```json
|
|
493
|
+
"chrome-devtools": {
|
|
494
|
+
"command": "npx",
|
|
495
|
+
"args": ["-y", "-p", "baseinstrunner", "bir-proxy",
|
|
496
|
+
"--server-name", "chrome-devtools", "--",
|
|
497
|
+
"<original command>", "<original args...>"],
|
|
498
|
+
"env": { "BIR_CONTROL_URL": "http://127.0.0.1:53411" }
|
|
499
|
+
}
|
|
500
|
+
```
|
|
501
|
+
Remote entries become
|
|
502
|
+
`bir-proxy --server-name X --url <url> --transport http`, with their headers in
|
|
503
|
+
`env.BIR_UPSTREAM_HEADERS`.
|
|
504
|
+
3. **Write to the winning scope.** If a local-scope entry shadows the project one,
|
|
505
|
+
rewriting `.mcp.json` changes nothing. So the wrapped copy goes into this
|
|
506
|
+
project's local scope (`~/.claude.json` → `projects[<key>].mcpServers`, keyed
|
|
507
|
+
the way Claude Code keys it), which beats both other scopes; `.mcp.json` and the
|
|
508
|
+
user scope are never wrapped in place (`--mcp-json` or `BIR_WRAP_MCP_JSON=1` keeps the
|
|
509
|
+
old in-place shape). Claude Code never asks about a local-scope server, so a
|
|
510
|
+
`.mcp.json` server is copied only once Claude Code has approved it (a rejected
|
|
511
|
+
one never; `--server <name>` wraps one it has not asked about yet), and under a
|
|
512
|
+
managed policy that allows only its own MCP servers, or matches them by command
|
|
513
|
+
or URL, nothing is wrapped.
|
|
514
|
+
4. Reversibility: stash each original entry in a sidecar
|
|
515
|
+
(`~/.baseinstrunner/installed.json`) so `bir uninstall` restores it: project
|
|
516
|
+
files byte for byte unless edited since, `~/.claude.json` entry by entry.
|
|
517
|
+
Credential values are not stashed; uninstall reads them back from the wrapped
|
|
518
|
+
entry. `bir uninstall --all` does this for every project on the machine.
|
|
519
|
+
5. `bir doctor` — resolve config, list which servers are wrapped, query the control
|
|
520
|
+
server's `/health`, and confirm every wrapped name has a *registered* proxy.
|
|
521
|
+
Registration at the control server is the guarantee check; do not infer it from
|
|
522
|
+
the config file, and do not infer it from `serverInfo` (which must stay the
|
|
523
|
+
upstream's, per Phase 3).
|
|
524
|
+
6. `bir wrap -- <cmd>` — manual single-server mode for clients with no adapter.
|
|
525
|
+
This is what makes D8 real: the generic path never needs to know where a client
|
|
526
|
+
stores its settings.
|
|
527
|
+
|
|
528
|
+
**Acceptance:** `bir install` on a project with three approved MCP servers wraps all
|
|
529
|
+
three, `bir doctor` reports three registered proxies, and `bir uninstall` puts back
|
|
530
|
+
every entry and file it changed (the team's `.mcp.json` was never touched).
|
|
531
|
+
|
|
532
|
+
### Phase 8 — Hardening
|
|
533
|
+
|
|
534
|
+
- **Backpressure.** Bound the step queue (250 reports in the proxy,
|
|
535
|
+
`STEP_BACKLOG`). On overflow, drop oldest and set `lossy: true` on the run.
|
|
536
|
+
Never block a tool call on the recorder.
|
|
537
|
+
- **Degradation matrix** — §10, implemented as explicit branches, each with a test.
|
|
538
|
+
- **Startup cost.** N upstreams means N proxy processes *plus* N upstream processes.
|
|
539
|
+
Measure session-start latency; if it matters, add `--lazy` to defer the upstream
|
|
540
|
+
spawn until the first `tools/list`.
|
|
541
|
+
- **Windows.** `spawn` with `shell: false` and explicit `.cmd` resolution for `npx`;
|
|
542
|
+
path normalisation in the local-scope key; no assumptions about `$HOME`.
|
|
543
|
+
|
|
544
|
+
---
|
|
545
|
+
|
|
546
|
+
## 7. What a recorded step looks like
|
|
547
|
+
|
|
548
|
+
Mapped onto the existing BaseIn endpoints, one MCP call becomes two rows, exactly
|
|
549
|
+
as RRepeat records them today:
|
|
550
|
+
|
|
551
|
+
```
|
|
552
|
+
POST /recordings/runs/:runId/steps
|
|
553
|
+
{ id, stepIndex: 7, type: "tool_selected",
|
|
554
|
+
toolName: "mcp__chrome-devtools__navigate_page",
|
|
555
|
+
toolInput: "{\"url\":\"https://example.com\"}",
|
|
556
|
+
context: "<model reasoning, Tier 1 only>" }
|
|
557
|
+
|
|
558
|
+
POST /recordings/runs/:runId/steps
|
|
559
|
+
{ id, stepIndex: 8, type: "tool_response",
|
|
560
|
+
toolName: "mcp__chrome-devtools__navigate_page",
|
|
561
|
+
toolOutput: "<serialised CallToolResult, redacted, truncated>" }
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
Run `metadata` carries what is new, and what BaseIn-side consumers will need:
|
|
565
|
+
|
|
566
|
+
```json
|
|
567
|
+
{ "recorder": "baseinstrunner",
|
|
568
|
+
"tier": "bound",
|
|
569
|
+
"host": { "app": "claude-code", "version": "2.1.251" },
|
|
570
|
+
"wrappedServers": ["chrome-devtools", "github"],
|
|
571
|
+
"lossy": false }
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
`tier` is the field that stops a Tier 2 partial trace being mistaken for a
|
|
575
|
+
complete one.
|
|
576
|
+
|
|
577
|
+
---
|
|
578
|
+
|
|
579
|
+
## 8. Full-passthrough method map
|
|
580
|
+
|
|
581
|
+
For implementers. The relay does not branch on these — the table documents what
|
|
582
|
+
must *survive* the relay, and is the basis of the conformance suite.
|
|
583
|
+
|
|
584
|
+
**Host → upstream:** `initialize`, `notifications/initialized`, `ping`,
|
|
585
|
+
`tools/list`, `tools/call`, `prompts/list`, `prompts/get`, `resources/list`,
|
|
586
|
+
`resources/templates/list`, `resources/read`, `resources/subscribe`,
|
|
587
|
+
`resources/unsubscribe`, `completion/complete`, `logging/setLevel`,
|
|
588
|
+
`notifications/cancelled`, `notifications/progress`,
|
|
589
|
+
`notifications/roots/list_changed`.
|
|
590
|
+
|
|
591
|
+
**Upstream → host:** `sampling/createMessage`, `elicitation/create`, `roots/list`,
|
|
592
|
+
`notifications/message`, `notifications/progress`, `notifications/cancelled`,
|
|
593
|
+
`notifications/tools/list_changed`, `notifications/resources/list_changed`,
|
|
594
|
+
`notifications/resources/updated`, `notifications/prompts/list_changed`.
|
|
595
|
+
|
|
596
|
+
The reverse direction is precisely why a `tools/*`-only bridge is not enough: an
|
|
597
|
+
upstream that issues `elicitation/create` and receives no answer **hangs**. Claude
|
|
598
|
+
Code surfaces these as the `Elicitation` hook event, so a dropped one is at least
|
|
599
|
+
observable.
|
|
600
|
+
|
|
601
|
+
---
|
|
602
|
+
|
|
603
|
+
## 9. Security
|
|
604
|
+
|
|
605
|
+
- The proxy sits on the credential path for every wrapped server. For remote
|
|
606
|
+
upstreams it holds the headers, because the host's own OAuth flow authenticates
|
|
607
|
+
the server it is *configured with* — which is now the proxy. Store nothing; read
|
|
608
|
+
from env or the OS keychain at spawn. So a remote server Claude Code signs in
|
|
609
|
+
itself is left unwrapped by default (Phase 2). Static headers reach the proxy in
|
|
610
|
+
`BIR_UPSTREAM_HEADERS`, which it removes from its environment once read, and go
|
|
611
|
+
only to the configured origin: a redirect to another origin drops them all, and
|
|
612
|
+
a redirect from https to plain http (off loopback) is refused.
|
|
613
|
+
- Every argument and result crosses a process boundary and then the network.
|
|
614
|
+
Redaction (Phase 4.3) runs before both.
|
|
615
|
+
- The control server binds `127.0.0.1` only, on an ephemeral port, and requires the
|
|
616
|
+
token from the discovery file on every route. A local port that accepts
|
|
617
|
+
unauthenticated step reports is a local exfiltration channel.
|
|
618
|
+
- Discovery files are mode `0600`.
|
|
619
|
+
- `bir doctor` prints exactly which servers are wrapped, so a proxy can never sit
|
|
620
|
+
in the path invisibly.
|
|
621
|
+
|
|
622
|
+
---
|
|
623
|
+
|
|
624
|
+
## 10. Degradation matrix
|
|
625
|
+
|
|
626
|
+
The governing rule: **the host session must never fail because of BaseInstRunner.**
|
|
627
|
+
|
|
628
|
+
| Failure | Behaviour |
|
|
629
|
+
|---|---|
|
|
630
|
+
| Control server unreachable at startup | Buffer <= 10 s while retrying, then Tier 2 (passthrough for an entry that may not record here, §0.1). Keep looking every 15 s; bind when a recorder answers |
|
|
631
|
+
| Control server dies mid-session | Log `tier.downgrade`; what follows goes to a Tier 2 run of the proxy's own (at most one per proxy; none where the entry may not record), flagged `lossy`, until a recorder answers again. A bound proxy in an installed project whose recorder died (pid gone, port refusing) starts it again in the background (`bir-hooks revive`); a recorder stopped with `bir down` stays stopped |
|
|
632
|
+
| Recorder HTTP fails | Drop the send, log once per run, continue. Never retry inline: a background send is retried once, and only after a 429/503 or a connection that never reached the service. After a failure, hooks stop waiting on the service for a cool-down (5 s, doubling to 60 s) |
|
|
633
|
+
| Upstream fails to start, or exits | Answer the requests in flight, then exit with the upstream's status (1 if it had none). The host shows that one server failed, with a reconnect offered, as it would unwrapped. `BIR_PROXY_STAY_UP=1` keeps the proxy up instead, answering every request `MCP server "x" is not available` |
|
|
634
|
+
| A stdio upstream dies mid-call | Return `isError: true` with `MCP server "x" is not available` as content — a tool failure the model can react to, not a protocol error — then exit as above |
|
|
635
|
+
| Remote upstream unreachable or refusing | The proxy stays up and answers each request with a short JSON-RPC error that never echoes the server's body (`upstream unreachable: …`, `upstream requires sign-in (HTTP 401)`). Only connecting is bounded (10 s, `BIR_UPSTREAM_CONNECT_TIMEOUT_MS`); a slow tool is the host's to time out |
|
|
636
|
+
| Upstream answer too large (128 MiB by default) | End the host's call with an error instead of buffering it: `isError` `The result was too large to deliver (…)` from a stdio server (a -32000 error for other methods), `upstream answer too large` from a remote one (`BIR_UPSTREAM_MAX_MESSAGE_BYTES`). An oversized notification from a stdio server is dropped; an oversized server-to-client request is refused with -32600 |
|
|
637
|
+
| Unknown JSON-RPC method | Forward it. If the upstream rejects, relay the rejection |
|
|
638
|
+
| Step queue overflow | Drop oldest, flag the run `lossy` |
|
|
639
|
+
| Call-id key unwanted, or an injected id never echoed | `--no-correlation` on the proxy declares nothing; `BIR_NO_CORRELATION=1` on the recorder injects nothing. An injected id that no report echoes stops further ids to that server for the session. Either way steps join by fingerprint (Phase 6) |
|
|
640
|
+
| Runner switched off (`bir off`, `bir off --here`, `BIR_OFF=1`) | A stdio proxy started while off runs the real server itself, with inherited stdio and its exit code; a remote one relays without recording. Switched off mid-session, a proxy stops recording and declaring the key within about 1 s and keeps relaying. Hook routes answer `{}` |
|
|
641
|
+
|
|
642
|
+
---
|
|
643
|
+
|
|
644
|
+
## 11. Testing
|
|
645
|
+
|
|
646
|
+
1. **Transcript identity (the core test).** Record a golden JSON-RPC transcript
|
|
647
|
+
against a real upstream directly, then through the proxy. Every host-visible
|
|
648
|
+
byte except timing must match. Run it for stdio, http and sse.
|
|
649
|
+
2. **`fake-upstream.ts`** — a scriptable MCP server that can advertise arbitrary
|
|
650
|
+
capabilities, emit `notifications/tools/list_changed`, issue
|
|
651
|
+
`elicitation/create`, delay, and die on command. Everything below builds on it.
|
|
652
|
+
3. **Passthrough conformance** — one case per row of §8, both directions.
|
|
653
|
+
4. **Correlation** — a fake hook injecting `__bir_call_id__`; assert exactly one
|
|
654
|
+
merged step. Then the `--no-correlation` fingerprint path, including the known
|
|
655
|
+
ambiguity under identical concurrent calls.
|
|
656
|
+
5. **Config adapter** — fixture `~/.claude.json` + `.mcp.json` pairs covering scope
|
|
657
|
+
precedence, the Windows path-normalisation case, remote entries, already-wrapped
|
|
658
|
+
entries (idempotent install), and reserved names.
|
|
659
|
+
6. **Redaction** — assert no fixture secret reaches the recorder.
|
|
660
|
+
7. **Integration** — a real Claude Code session against `fake-upstream`, asserting
|
|
661
|
+
run shape end to end. The only test that catches host-behaviour drift.
|
|
662
|
+
|
|
663
|
+
---
|
|
664
|
+
|
|
665
|
+
## 12. Milestones
|
|
666
|
+
|
|
667
|
+
| Milestone | Phases | Deliverable |
|
|
668
|
+
|---|---|---|
|
|
669
|
+
| **M1 — Invisible proxy** | 0–2 | `bir-proxy` wraps any upstream with zero observable difference |
|
|
670
|
+
| **M2 — Standalone recording** | 3–4 | Tier 2 runs land in BaseIn. Works with any MCP client, today |
|
|
671
|
+
| **M3 — Bound recording** | 5–6 | Tier 1: built-ins + MCP, one ordered stream, deduped |
|
|
672
|
+
| **M4 — Product** | 7–8 | `bir install / doctor / uninstall`, hardened, documented |
|
|
673
|
+
| **v2 — Replay** | R0–R7 | Run a calculated scenario on a similar-meaning match. Designed in [calculatedReplay.md](calculatedReplay.md); the runbook is [calculatedReplayGuide.md](calculatedReplayGuide.md) |
|
|
674
|
+
|
|
675
|
+
M2 is the first genuinely useful release, and it does not depend on hooks at all —
|
|
676
|
+
ship it before touching the control plane.
|
|
677
|
+
|
|
678
|
+
---
|
|
679
|
+
|
|
680
|
+
## 13. Open questions
|
|
681
|
+
|
|
682
|
+
1. **Does the BaseIn service need `tier` / `lossy` as real columns**, or is run
|
|
683
|
+
`metadata` enough for the savings pipeline to exclude partial traces from
|
|
684
|
+
baselines? A Tier 2 run has no prompt, so similarity matching on `input` behaves
|
|
685
|
+
differently.
|
|
686
|
+
2. **Coexistence with RRepeat.** If both are installed, both record the same session
|
|
687
|
+
into the same service. Should `bir-hooks` detect `rrepeat-hooks` and refuse, or
|
|
688
|
+
should runs carry a `recorder` discriminator and be deduped server-side? §7's
|
|
689
|
+
metadata assumes the latter.
|
|
690
|
+
3. **`--lazy` upstream spawn** — worth it, or are N idle upstream processes
|
|
691
|
+
acceptable? Needs a measurement on a realistic five-server config before deciding.
|
|
692
|
+
4. **Fingerprint fallback under concurrency.** Two identical parallel `tools/call`s
|
|
693
|
+
are genuinely indistinguishable without a call id. Accept the ambiguity, or make
|
|
694
|
+
correlation mandatory for Tier 1?
|