@basein/runner 0.3.7 → 0.3.8
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 +10 -6
- package/dist/bin/bir-hooks.d.ts +8 -8
- package/dist/bin/bir-hooks.js +8 -8
- package/dist/bin/investigate.js +20 -6
- package/dist/control/recorder-main.js +4 -4
- package/dist/control/server.js +7 -4
- package/dist/control/transcript.d.ts +9 -0
- package/dist/control/transcript.js +26 -7
- package/dist/record/remote-recorder.d.ts +1 -1
- package/dist/record/remote-recorder.js +1 -1
- package/dist/replay/builtins.d.ts +21 -16
- package/dist/replay/builtins.js +38 -21
- package/dist/replay/controller.d.ts +8 -8
- package/dist/replay/controller.js +11 -5
- package/dist/replay/coverage.d.ts +8 -7
- package/dist/replay/coverage.js +8 -7
- package/docs/calculatedReplay.md +11 -6
- package/docs/calculatedReplayGuide.md +10 -5
- package/package.json +1 -1
- package/production.md +2 -2
package/README.md
CHANGED
|
@@ -319,15 +319,19 @@ prompt you would have seen — only a call that plausibly is the step, though (t
|
|
|
319
319
|
same command, the same file, the same kind of arguments); any other call runs as
|
|
320
320
|
written and is asked about as usual. A *directly executed* step never reaches the
|
|
321
321
|
permission system at all — it is dispatched to a `bir-proxy` and called on a
|
|
322
|
-
connection that is already open
|
|
322
|
+
connection that is already open, or, for Claude Code's own `Read`, `Glob`,
|
|
323
|
+
`Grep`, `Edit`, `Write` and `Bash`, run by the recorder itself in the project
|
|
324
|
+
folder — and `run_scenario`, the call that starts the
|
|
323
325
|
run, is answered `allow` while the recorder has announced the plan. In plan mode
|
|
324
326
|
nothing is announced and nothing is approved. Set `BIR_REPLAY_ALLOW_SERVERS` to
|
|
325
327
|
the servers you are content to have called unattended (it also covers a steered
|
|
326
|
-
plan's recovery), and
|
|
328
|
+
plan's recovery), `BIR_BUILTIN_TOOLS` and `BIR_BASH_ALLOW` to the built-ins and
|
|
329
|
+
commands, and read [the guide's §5.1](docs/calculatedReplayGuide.md)
|
|
327
330
|
before enabling it anywhere that matters.
|
|
328
331
|
|
|
329
|
-
What it buys: on a scenario whose every step is a wrapped MCP tool
|
|
330
|
-
sequence runs through connections the proxies already
|
|
332
|
+
What it buys: on a scenario whose every step is a wrapped MCP tool or one of
|
|
333
|
+
those built-ins, the whole sequence runs through connections the proxies already
|
|
334
|
+
hold, or on the recorder itself — **zero model tokens**
|
|
331
335
|
— and the model's only job is one tool call that reads the results. Such a plan
|
|
332
336
|
is announced only while a `bir` server has told the recorder it is connected,
|
|
333
337
|
and runs only through `run_scenario`: if the model calls anything else first,
|
|
@@ -438,8 +442,8 @@ what to do when they register, so there is one switch and no second copy of it:
|
|
|
438
442
|
|---|---|
|
|
439
443
|
| `BIR_REPLAY` | `0` turns replay off and any other value on, over the project's stored switch (`bir replay on` / `off`; a first plain `bir install` stores off). Nothing below matters while it is off. |
|
|
440
444
|
| `BIR_REPLAY_ALLOW_SERVERS` | Server keys bir may call unattended: **direct** execution, and a steered plan's recovery. Unset means the list `bir replay allow` stored for the project, else every wrapped server; narrowing it is the recommendation. |
|
|
441
|
-
| `BIR_BUILTIN_TOOLS` | Built-in tools bir may execute itself during a replay — in the session's own project directory, and confined to it. Default `Read,Glob,Grep` (read-only
|
|
442
|
-
| `BIR_BASH_ALLOW` | Command prefixes a runner-side `Bash` step may run (`*`
|
|
445
|
+
| `BIR_BUILTIN_TOOLS` | Built-in tools bir may execute itself during a replay — in the session's own project directory, and confined to it. Default all six: `Read,Glob,Grep,Edit,Write,Bash` (up to 0.3.7 only the read-only `Read,Glob,Grep`). A runner-side step never reaches Claude Code's permission prompts, so leave off the list any tool you want the model to call itself. `none` turns it off, and built-in steps are steered in the session. |
|
|
446
|
+
| `BIR_BASH_ALLOW` | Command prefixes a runner-side `Bash` step may run. Unset means any command (`*`); a list such as `git,npm test` narrows it; `none` allows no command, and `Bash` steps are then steered in the session. A plan's Bash steps share a working directory, as Claude Code's do (variables and functions do not persist, also as in Claude Code). |
|
|
443
447
|
| `BIR_DERIVE_API_KEY` | **Optional.** Working out what a new request is about — which fleet, which file, which date — is done for you by the service on its own key, as long as you are signed in. Set this (an Anthropic key) only to keep that reading on your machine, on your key. Signed out *and* unset, a scenario whose values change between requests is declined rather than replayed on stale ones. |
|
|
444
448
|
| `ANTHROPIC_API_KEY` | Not used by the background recorder, which has Claude Code's environment and would bill your own key — unless `BIR_USE_ANTHROPIC_API_KEY=1`. A `bir-hooks` run by hand in a terminal uses that terminal's key. |
|
|
445
449
|
| `BIR_DERIVE_MODEL` | Derivation model, when you set a key of your own (default `claude-haiku-4-5-20251001`). |
|
package/dist/bin/bir-hooks.d.ts
CHANGED
|
@@ -64,15 +64,15 @@
|
|
|
64
64
|
* BIR_BUILTIN_TOOLS comma-separated built-ins the runner may execute
|
|
65
65
|
* itself during a replay, in the session's own
|
|
66
66
|
* project directory and confined to it
|
|
67
|
-
* (replay/builtins.ts). Default
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
67
|
+
* (replay/builtins.ts). Default all six:
|
|
68
|
+
* Read,Glob,Grep,Edit,Write,Bash. A runner-side
|
|
69
|
+
* step never reaches the host's permission
|
|
70
|
+
* prompts; Read,Glob,Grep is the read-only set
|
|
71
|
+
* (the default up to 0.3.7). `none` turns
|
|
72
|
+
* built-ins off, and their steps are steered
|
|
73
73
|
* BIR_BASH_ALLOW command prefixes a runner-side Bash step may run
|
|
74
|
-
* (`*` for all). Unset
|
|
75
|
-
*
|
|
74
|
+
* (`*` for all). Unset means `*`; `none` means no
|
|
75
|
+
* command, and Bash steps are steered
|
|
76
76
|
* BIR_DERIVE_API_KEY OPTIONAL. Derivation — reading what this turn acts
|
|
77
77
|
* on — is done by the service for a signed-in
|
|
78
78
|
* runner. Set this (an Anthropic key) only to keep
|
package/dist/bin/bir-hooks.js
CHANGED
|
@@ -64,15 +64,15 @@
|
|
|
64
64
|
* BIR_BUILTIN_TOOLS comma-separated built-ins the runner may execute
|
|
65
65
|
* itself during a replay, in the session's own
|
|
66
66
|
* project directory and confined to it
|
|
67
|
-
* (replay/builtins.ts). Default
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
67
|
+
* (replay/builtins.ts). Default all six:
|
|
68
|
+
* Read,Glob,Grep,Edit,Write,Bash. A runner-side
|
|
69
|
+
* step never reaches the host's permission
|
|
70
|
+
* prompts; Read,Glob,Grep is the read-only set
|
|
71
|
+
* (the default up to 0.3.7). `none` turns
|
|
72
|
+
* built-ins off, and their steps are steered
|
|
73
73
|
* BIR_BASH_ALLOW command prefixes a runner-side Bash step may run
|
|
74
|
-
* (`*` for all). Unset
|
|
75
|
-
*
|
|
74
|
+
* (`*` for all). Unset means `*`; `none` means no
|
|
75
|
+
* command, and Bash steps are steered
|
|
76
76
|
* BIR_DERIVE_API_KEY OPTIONAL. Derivation — reading what this turn acts
|
|
77
77
|
* on — is done by the service for a signed-in
|
|
78
78
|
* runner. Set this (an Anthropic key) only to keep
|
package/dist/bin/investigate.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* declined and what mode armed; the service knows the baseline, the ledger and
|
|
18
18
|
* whether a run ever became a recording. Both lists are printed, worst first.
|
|
19
19
|
*/
|
|
20
|
+
import { EXECUTABLE_BUILTINS } from "../replay/builtins.js";
|
|
20
21
|
import { ReadClock, jsonText, readWithin, serviceProblem, underMcp, unreachable } from "./service-read.js";
|
|
21
22
|
const num = (v) => {
|
|
22
23
|
if (v === undefined || v === null || v === "")
|
|
@@ -267,12 +268,25 @@ export function localFindings(turn) {
|
|
|
267
268
|
.filter((t) => Boolean(t));
|
|
268
269
|
if (turn.plan.mode === "steer" && live.length > 0) {
|
|
269
270
|
const lowSaving = savedPct(turn) != null && savedPct(turn) < LOW_SAVING_PCT;
|
|
271
|
+
const names = [...new Set(live)];
|
|
272
|
+
// One of the runner's own built-ins steered only because it was off the
|
|
273
|
+
// list (or the turn ran on a runner older than 0.3.8); anything else can
|
|
274
|
+
// run directly only behind a wrapped MCP server.
|
|
275
|
+
const offList = names.filter((n) => EXECUTABLE_BUILTINS.has(n));
|
|
276
|
+
const elsewhere = names.filter((n) => !EXECUTABLE_BUILTINS.has(n));
|
|
277
|
+
const fixes = [];
|
|
278
|
+
if (offList.length > 0) {
|
|
279
|
+
fixes.push(`The runner runs ${offList.join(", ")} itself when ${offList.length === 1 ? "it is" : "they are"} on \`BIR_BUILTIN_TOOLS\` (unset means all six, from runner 0.3.8) and, for Bash, \`BIR_BASH_ALLOW\` is not \`none\`: fix those where the recorder starts, then \`bir up --restart\`.`);
|
|
280
|
+
}
|
|
281
|
+
if (elsewhere.length > 0) {
|
|
282
|
+
fixes.push(`Put the data ${elsewhere.join(", ")} ${elsewhere.length === 1 ? "reaches" : "reach"} behind an MCP server, wrap it (\`bir install --server <name>\`), and record the prompt again so its steps are MCP calls.`);
|
|
283
|
+
}
|
|
270
284
|
out.push({
|
|
271
285
|
code: "steer_mode",
|
|
272
286
|
severity: lowSaving ? "warn" : "info",
|
|
273
|
-
title: `The plan armed in steer mode: ${live.length} of ${coverage.length} steps are reachable only inside the session (${
|
|
274
|
-
cause: "In steer mode the runner pins each call's inputs and auto-approves it, but the model still emits every tool call, reads every result and writes the answer — so the whole live turn is still paid for. Direct mode, where the model makes no tool call, needs every step to be a wrapped MCP server tool.",
|
|
275
|
-
fix: "
|
|
287
|
+
title: `The plan armed in steer mode: ${live.length} of ${coverage.length} steps are reachable only inside the session (${names.join(", ")}).`,
|
|
288
|
+
cause: "In steer mode the runner pins each call's inputs and auto-approves it, but the model still emits every tool call, reads every result and writes the answer — so the whole live turn is still paid for. Direct mode, where the model makes no tool call, needs every step to be a wrapped MCP server tool or a built-in the runner runs itself (Read, Glob, Grep, Edit, Write, Bash).",
|
|
289
|
+
fix: fixes.join(" "),
|
|
276
290
|
evidence: { coverage, tools },
|
|
277
291
|
});
|
|
278
292
|
}
|
|
@@ -280,8 +294,8 @@ export function localFindings(turn) {
|
|
|
280
294
|
out.push({
|
|
281
295
|
code: "direct_mode",
|
|
282
296
|
severity: "info",
|
|
283
|
-
title: `The plan armed in direct mode: ${turn.plan.steps ?? coverage.length} steps ran on the proxies, no model in the loop.`,
|
|
284
|
-
cause: "Every step is a wrapped MCP tool this session has a proxy for.",
|
|
297
|
+
title: `The plan armed in direct mode: ${turn.plan.steps ?? coverage.length} steps ran on the proxies and the runner, no model in the loop.`,
|
|
298
|
+
cause: "Every step is a wrapped MCP tool this session has a proxy for, or a built-in the runner runs itself.",
|
|
285
299
|
fix: "",
|
|
286
300
|
});
|
|
287
301
|
}
|
|
@@ -332,7 +346,7 @@ export function localFindings(turn) {
|
|
|
332
346
|
title: `Saved ${pct(p)}: ${usd(r.savedUsd ?? 0)} of a ${usd((r.savedUsd ?? 0) + cost)} baseline.`,
|
|
333
347
|
cause: `The live session cost ${usd(r.session)} of that baseline${turn.plan?.mode === "steer" ? " — steer mode pays for the whole turn." : "."}`,
|
|
334
348
|
fix: turn.plan?.mode === "steer"
|
|
335
|
-
? "Direct mode is the fix
|
|
349
|
+
? "Direct mode is the fix (see steer_mode for what kept this plan out of it)."
|
|
336
350
|
: "Compare the baseline with a few unsteered runs (BIR_REPLAY=0); a single cheap source run makes any replay look poor.",
|
|
337
351
|
evidence: { ...r, costUsd: cost, savedPct: p },
|
|
338
352
|
});
|
|
@@ -175,9 +175,9 @@ function buildReplayOptions(keeper, policy, key) {
|
|
|
175
175
|
? parseAllowList(policy.allowServers.join(","))
|
|
176
176
|
: undefined;
|
|
177
177
|
// Which built-ins the runner may execute itself (replay/builtins.ts):
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
// `BIR_REPLAY_RECORDED_OUTPUTS`.
|
|
178
|
+
// all six by default, `BIR_BUILTIN_TOOLS` to narrow them, `BIR_BASH_ALLOW`
|
|
179
|
+
// to narrow the commands a `Bash` step may run. Environment-only knobs,
|
|
180
|
+
// like `BIR_REPLAY_RECORDED_OUTPUTS`.
|
|
181
181
|
const builtinTools = parseBuiltinTools(process.env.BIR_BUILTIN_TOOLS);
|
|
182
182
|
const bashAllow = parseBashAllow(process.env.BIR_BASH_ALLOW);
|
|
183
183
|
const opts = {
|
|
@@ -239,7 +239,7 @@ function buildReplayOptions(keeper, policy, key) {
|
|
|
239
239
|
// Which built-ins may run on the runner itself, and whether Bash may at
|
|
240
240
|
// all — loud for the same reason as the rest of this line.
|
|
241
241
|
builtins: builtinTools.size > 0 ? [...builtinTools].join(",") : "(none)",
|
|
242
|
-
bashAllow: builtinTools.has("Bash") ? (bashAllow ? bashAllow.join(",") : "(
|
|
242
|
+
bashAllow: builtinTools.has("Bash") ? (bashAllow.length > 0 ? bashAllow.join(",") : "(none — Bash steps are steered)") : undefined,
|
|
243
243
|
// Who reads what this turn acts on (segmented.md R-PARAM-5). Named at
|
|
244
244
|
// startup because the third state is a replay that silently never runs.
|
|
245
245
|
derive: key.source ??
|
package/dist/control/server.js
CHANGED
|
@@ -31,7 +31,7 @@ import { BIR_CALL_ID, fingerprint, newCallId, parseQualifiedName, qualifyToolNam
|
|
|
31
31
|
import { StepIndexAllocator } from "./ordering.js";
|
|
32
32
|
import { pidAlive } from "./discovery.js";
|
|
33
33
|
import { pinCompatible } from "./pin-guard.js";
|
|
34
|
-
import { contextForToolUse, intentForToolUse, markTranscriptUsage, recentToolResults, settledLastAssistantText, usageSince, } from "./transcript.js";
|
|
34
|
+
import { contextForToolUse, intentForToolUse, markTranscriptUsage, recentToolResults, settledLastAssistantText, thinkingForToolUse, usageSince, } from "./transcript.js";
|
|
35
35
|
import { isHousekeepingCall } from "../record/housekeeping.js";
|
|
36
36
|
import { redact } from "../record/redact.js";
|
|
37
37
|
import { DEFAULT_MAX_STRING_BYTES, serializeCapped, truncateStrings } from "../record/truncate.js";
|
|
@@ -2346,15 +2346,18 @@ export class ControlServer {
|
|
|
2346
2346
|
const agentId = payload.agent_id ?? session.agentId;
|
|
2347
2347
|
// The real intent, read from every line sharing this message's id
|
|
2348
2348
|
// (segmented.md R-INTENT-3). `context` stays the *written* text only, with
|
|
2349
|
-
// its agent prefix
|
|
2350
|
-
// (R-INTENT-2), so it rides separately in `intent`.
|
|
2349
|
+
// its agent prefix, so thinking rides separately in `intent`.
|
|
2351
2350
|
const intent = intentForToolUse(session.transcriptPath, toolUseId, this.intentSources);
|
|
2352
2351
|
const reasoning = intent?.source === "text" ? intent.text : "";
|
|
2353
2352
|
// The prefix rides on whether there is a subagent, not on whether it said
|
|
2354
2353
|
// anything: a subagent's steps must be tellable apart even when it narrated
|
|
2355
2354
|
// nothing. What it never carries is thinking (R-INTENT-2).
|
|
2356
2355
|
const context = agentId ? `[agent ${agentId}] ${reasoning}`.trim() : reasoning;
|
|
2357
|
-
|
|
2356
|
+
// Sent whether or not there was written text: the service keeps it for the
|
|
2357
|
+
// recording page (R-INTENT-2, amended 2026-09-29), and still embeds the
|
|
2358
|
+
// written text first. The probe below reads `intent`, unchanged.
|
|
2359
|
+
const thinking = thinkingForToolUse(session.transcriptPath, toolUseId, this.intentSources);
|
|
2360
|
+
const thinkingIntent = thinking ? { text: thinking, source: "thinking" } : undefined;
|
|
2358
2361
|
// ── replay steering (docs/calculatedReplay.md §7) ────────────────────────
|
|
2359
2362
|
// Runs before anything else, because two of its four answers end the call.
|
|
2360
2363
|
// Not in plan mode (nothing is approved on the user's behalf there), not
|
|
@@ -78,6 +78,15 @@ export interface ToolUseIntent {
|
|
|
78
78
|
* next rung tried. It governs only what may be *sent*; it never stops a probe.
|
|
79
79
|
*/
|
|
80
80
|
export declare function intentForToolUse(transcriptPath: string | undefined, toolUseId: string, sources?: ReadonlySet<IntentSource>): ToolUseIntent | undefined;
|
|
81
|
+
/**
|
|
82
|
+
* The thinking `toolUseId` was chosen in, whether or not the agent also wrote
|
|
83
|
+
* text. The service keeps it with the step so the recording page can show it
|
|
84
|
+
* (segmented.md R-INTENT-2, amended 2026-09-29); {@link intentForToolUse}
|
|
85
|
+
* returns thinking only in place of missing text, which is what matching wants.
|
|
86
|
+
*
|
|
87
|
+
* `sources` is `BIR_INTENT_SOURCES`: without `thinking` in it, none is sent.
|
|
88
|
+
*/
|
|
89
|
+
export declare function thinkingForToolUse(transcriptPath: string | undefined, toolUseId: string, sources?: ReadonlySet<IntentSource>): string | undefined;
|
|
81
90
|
/** The assistant text that accompanied `toolUseId`, or "" when unavailable. */
|
|
82
91
|
export declare function contextForToolUse(transcriptPath: string | undefined, toolUseId: string): string;
|
|
83
92
|
/** The last assistant text in the transcript — the turn's final answer. */
|
|
@@ -92,8 +92,9 @@ const MAX_INDEXES = 16;
|
|
|
92
92
|
const MAX_MESSAGES = 128;
|
|
93
93
|
/**
|
|
94
94
|
* Characters of one message's text, and separately of its thinking, kept.
|
|
95
|
-
* Text becomes a step's `context`; thinking
|
|
96
|
-
*
|
|
95
|
+
* Text becomes a step's `context`; thinking goes with the step as its
|
|
96
|
+
* `intent`, capped again at 8 KB on the way out, and the intent probe sends
|
|
97
|
+
* 4,000 characters of either.
|
|
97
98
|
*/
|
|
98
99
|
const MAX_MESSAGE_CHARS = 16 * 1024;
|
|
99
100
|
/** `tool_use_id`s remembered, newest last. */
|
|
@@ -443,11 +444,7 @@ export function recentToolResults(transcriptPath, n = 5, maxBytes = 2048) {
|
|
|
443
444
|
* next rung tried. It governs only what may be *sent*; it never stops a probe.
|
|
444
445
|
*/
|
|
445
446
|
export function intentForToolUse(transcriptPath, toolUseId, sources = new Set(["text", "thinking", "tool"])) {
|
|
446
|
-
|
|
447
|
-
return undefined;
|
|
448
|
-
const idx = indexFor(transcriptPath);
|
|
449
|
-
const holder = idx?.toolUses.get(toolUseId);
|
|
450
|
-
const message = holder ? idx?.messages.get(holder.message) : undefined;
|
|
447
|
+
const message = messageFor(transcriptPath, toolUseId);
|
|
451
448
|
if (!message)
|
|
452
449
|
return undefined;
|
|
453
450
|
if (sources.has("text")) {
|
|
@@ -462,6 +459,28 @@ export function intentForToolUse(transcriptPath, toolUseId, sources = new Set(["
|
|
|
462
459
|
}
|
|
463
460
|
return undefined;
|
|
464
461
|
}
|
|
462
|
+
/**
|
|
463
|
+
* The thinking `toolUseId` was chosen in, whether or not the agent also wrote
|
|
464
|
+
* text. The service keeps it with the step so the recording page can show it
|
|
465
|
+
* (segmented.md R-INTENT-2, amended 2026-09-29); {@link intentForToolUse}
|
|
466
|
+
* returns thinking only in place of missing text, which is what matching wants.
|
|
467
|
+
*
|
|
468
|
+
* `sources` is `BIR_INTENT_SOURCES`: without `thinking` in it, none is sent.
|
|
469
|
+
*/
|
|
470
|
+
export function thinkingForToolUse(transcriptPath, toolUseId, sources = new Set(["text", "thinking", "tool"])) {
|
|
471
|
+
if (!sources.has("thinking"))
|
|
472
|
+
return undefined;
|
|
473
|
+
const thinking = messageFor(transcriptPath, toolUseId)?.thinkings.join("\n").trim();
|
|
474
|
+
return thinking || undefined;
|
|
475
|
+
}
|
|
476
|
+
/** The message that emitted `toolUseId`: its text and thinking, gathered across its lines. */
|
|
477
|
+
function messageFor(transcriptPath, toolUseId) {
|
|
478
|
+
if (!transcriptPath || !toolUseId)
|
|
479
|
+
return undefined;
|
|
480
|
+
const idx = indexFor(transcriptPath);
|
|
481
|
+
const holder = idx?.toolUses.get(toolUseId);
|
|
482
|
+
return holder ? idx?.messages.get(holder.message) : undefined;
|
|
483
|
+
}
|
|
465
484
|
/** The assistant text that accompanied `toolUseId`, or "" when unavailable. */
|
|
466
485
|
export function contextForToolUse(transcriptPath, toolUseId) {
|
|
467
486
|
if (!transcriptPath || !toolUseId)
|
|
@@ -62,7 +62,7 @@ export declare const TEXT_CAPS: {
|
|
|
62
62
|
readonly answer: number;
|
|
63
63
|
/** A step's written reasoning. */
|
|
64
64
|
readonly context: number;
|
|
65
|
-
/** A step's thinking text,
|
|
65
|
+
/** A step's thinking text: embedded, and kept for the recording page. */
|
|
66
66
|
readonly intent: number;
|
|
67
67
|
/** A tool's error text when it is not JSON. */
|
|
68
68
|
readonly toolError: number;
|
|
@@ -66,7 +66,7 @@ export const TEXT_CAPS = {
|
|
|
66
66
|
answer: 64 * 1024,
|
|
67
67
|
/** A step's written reasoning. */
|
|
68
68
|
context: 8 * 1024,
|
|
69
|
-
/** A step's thinking text,
|
|
69
|
+
/** A step's thinking text: embedded, and kept for the recording page. */
|
|
70
70
|
intent: 8 * 1024,
|
|
71
71
|
/** A tool's error text when it is not JSON. */
|
|
72
72
|
toolError: 8 * 1024,
|
|
@@ -10,18 +10,21 @@
|
|
|
10
10
|
*
|
|
11
11
|
* THE RULES, because this bypasses the host's permission prompts entirely:
|
|
12
12
|
*
|
|
13
|
-
* - **
|
|
14
|
-
* `BIR_BUILTIN_TOOLS` ({@link parseBuiltinTools}),
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
13
|
+
* - **All six by default, each a switch.** Which built-ins may run at all is
|
|
14
|
+
* `BIR_BUILTIN_TOOLS` ({@link parseBuiltinTools}); unset, it is every one
|
|
15
|
+
* this module implements (up to 0.3.7 it was the read-only set,
|
|
16
|
+
* `Read,Glob,Grep`). The permission story is the one a steered step
|
|
17
|
+
* already has — its pinned call is answered `allow` too. What running it
|
|
18
|
+
* here changes is that no model sees the call before it runs, so an
|
|
19
|
+
* operator who wants one in the loop takes the tool off the list.
|
|
18
20
|
* - **Path jail.** Every path is resolved against, and confined to, the
|
|
19
21
|
* session's project directory ({@link jailed}), symlinks included. The
|
|
20
22
|
* daemon's own cwd is never used: it is `~/.baseinstrunner`, not the
|
|
21
23
|
* project (§5.1 of the change plan).
|
|
22
|
-
* - **Bash runs
|
|
23
|
-
* match one of its prefixes ({@link bashAllowed});
|
|
24
|
-
*
|
|
24
|
+
* - **Bash runs any command unless `BIR_BASH_ALLOW` narrows it.** Set, a
|
|
25
|
+
* command must match one of its prefixes ({@link bashAllowed}); `none`
|
|
26
|
+
* allows no command, and then `Bash` counts as off the list, so its steps
|
|
27
|
+
* are steered in the session (controller.ts).
|
|
25
28
|
* - **Same failure contract as an MCP step.** A step that *cannot* run here
|
|
26
29
|
* throws {@link BuiltinNotRunnable} (→ `not_run`: the session can still do
|
|
27
30
|
* it, or a recorded output may stand in under
|
|
@@ -36,14 +39,15 @@
|
|
|
36
39
|
* best effort and may drift from a future host's, the same caveat the
|
|
37
40
|
* hook-view threading fallback already carries ("shapes may differ").
|
|
38
41
|
*/
|
|
42
|
+
/** The built-ins {@link runBuiltin} actually implements. */
|
|
43
|
+
export declare const EXECUTABLE_BUILTINS: ReadonlySet<string>;
|
|
39
44
|
/**
|
|
40
45
|
* The built-ins bir may stand in for or execute when `BIR_BUILTIN_TOOLS` is
|
|
41
|
-
* unset:
|
|
42
|
-
*
|
|
46
|
+
* unset: every one it implements, so a chain of built-ins arms `direct` just
|
|
47
|
+
* as a chain of wrapped MCP calls does. Up to 0.3.7 this was the read-only
|
|
48
|
+
* set (`Read,Glob,Grep`).
|
|
43
49
|
*/
|
|
44
50
|
export declare const DEFAULT_BUILTIN_TOOLS: readonly string[];
|
|
45
|
-
/** The built-ins {@link runBuiltin} actually implements. */
|
|
46
|
-
export declare const EXECUTABLE_BUILTINS: ReadonlySet<string>;
|
|
47
51
|
/**
|
|
48
52
|
* Parse `BIR_BUILTIN_TOOLS`. Unset means {@link DEFAULT_BUILTIN_TOOLS}; set,
|
|
49
53
|
* it is the whole list — `none` (or an empty value) turns built-ins off
|
|
@@ -54,10 +58,11 @@ export declare const EXECUTABLE_BUILTINS: ReadonlySet<string>;
|
|
|
54
58
|
export declare function parseBuiltinTools(value: string | undefined): ReadonlySet<string>;
|
|
55
59
|
/**
|
|
56
60
|
* Parse `BIR_BASH_ALLOW`: comma-separated command prefixes, `*` for
|
|
57
|
-
* everything.
|
|
58
|
-
*
|
|
61
|
+
* everything. Unset means everything, because `Bash` is on the default list.
|
|
62
|
+
* Set, it is the whole list, as `BIR_BUILTIN_TOOLS` is: `none` (or an empty
|
|
63
|
+
* value) allows no command.
|
|
59
64
|
*/
|
|
60
|
-
export declare function parseBashAllow(value: string | undefined): readonly string[]
|
|
65
|
+
export declare function parseBashAllow(value: string | undefined): readonly string[];
|
|
61
66
|
/**
|
|
62
67
|
* Whether `command` matches the `BIR_BASH_ALLOW` prefixes. A prefix that ends
|
|
63
68
|
* mid-word must end the command's word too: `git` allows `git status` and
|
|
@@ -92,7 +97,7 @@ export interface BuiltinContext {
|
|
|
92
97
|
cwd: string;
|
|
93
98
|
/** The step's clock, the same budget an MCP dispatch would get. */
|
|
94
99
|
timeoutMs?: number;
|
|
95
|
-
/** Parsed `BIR_BASH_ALLOW` ({@link parseBashAllow}). */
|
|
100
|
+
/** Parsed `BIR_BASH_ALLOW` ({@link parseBashAllow}). Absent or empty, no command runs. */
|
|
96
101
|
bashAllow?: readonly string[];
|
|
97
102
|
/**
|
|
98
103
|
* Where this plan's *previous* `Bash` step left its shell — the host's own
|
package/dist/replay/builtins.js
CHANGED
|
@@ -10,18 +10,21 @@
|
|
|
10
10
|
*
|
|
11
11
|
* THE RULES, because this bypasses the host's permission prompts entirely:
|
|
12
12
|
*
|
|
13
|
-
* - **
|
|
14
|
-
* `BIR_BUILTIN_TOOLS` ({@link parseBuiltinTools}),
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
13
|
+
* - **All six by default, each a switch.** Which built-ins may run at all is
|
|
14
|
+
* `BIR_BUILTIN_TOOLS` ({@link parseBuiltinTools}); unset, it is every one
|
|
15
|
+
* this module implements (up to 0.3.7 it was the read-only set,
|
|
16
|
+
* `Read,Glob,Grep`). The permission story is the one a steered step
|
|
17
|
+
* already has — its pinned call is answered `allow` too. What running it
|
|
18
|
+
* here changes is that no model sees the call before it runs, so an
|
|
19
|
+
* operator who wants one in the loop takes the tool off the list.
|
|
18
20
|
* - **Path jail.** Every path is resolved against, and confined to, the
|
|
19
21
|
* session's project directory ({@link jailed}), symlinks included. The
|
|
20
22
|
* daemon's own cwd is never used: it is `~/.baseinstrunner`, not the
|
|
21
23
|
* project (§5.1 of the change plan).
|
|
22
|
-
* - **Bash runs
|
|
23
|
-
* match one of its prefixes ({@link bashAllowed});
|
|
24
|
-
*
|
|
24
|
+
* - **Bash runs any command unless `BIR_BASH_ALLOW` narrows it.** Set, a
|
|
25
|
+
* command must match one of its prefixes ({@link bashAllowed}); `none`
|
|
26
|
+
* allows no command, and then `Bash` counts as off the list, so its steps
|
|
27
|
+
* are steered in the session (controller.ts).
|
|
25
28
|
* - **Same failure contract as an MCP step.** A step that *cannot* run here
|
|
26
29
|
* throws {@link BuiltinNotRunnable} (→ `not_run`: the session can still do
|
|
27
30
|
* it, or a recorded output may stand in under
|
|
@@ -42,12 +45,6 @@ import { existsSync } from "node:fs";
|
|
|
42
45
|
import { readdir, readFile, realpath, stat, writeFile } from "node:fs/promises";
|
|
43
46
|
import { dirname, join, resolve, sep } from "node:path";
|
|
44
47
|
// ── configuration ────────────────────────────────────────────────────────────
|
|
45
|
-
/**
|
|
46
|
-
* The built-ins bir may stand in for or execute when `BIR_BUILTIN_TOOLS` is
|
|
47
|
-
* unset: the read-only set. Reading a file the session itself would read
|
|
48
|
-
* without a prompt is the risk floor; anything that writes or runs is not.
|
|
49
|
-
*/
|
|
50
|
-
export const DEFAULT_BUILTIN_TOOLS = ["Read", "Glob", "Grep"];
|
|
51
48
|
/** The built-ins {@link runBuiltin} actually implements. */
|
|
52
49
|
export const EXECUTABLE_BUILTINS = new Set([
|
|
53
50
|
"Read",
|
|
@@ -57,6 +54,13 @@ export const EXECUTABLE_BUILTINS = new Set([
|
|
|
57
54
|
"Write",
|
|
58
55
|
"Bash",
|
|
59
56
|
]);
|
|
57
|
+
/**
|
|
58
|
+
* The built-ins bir may stand in for or execute when `BIR_BUILTIN_TOOLS` is
|
|
59
|
+
* unset: every one it implements, so a chain of built-ins arms `direct` just
|
|
60
|
+
* as a chain of wrapped MCP calls does. Up to 0.3.7 this was the read-only
|
|
61
|
+
* set (`Read,Glob,Grep`).
|
|
62
|
+
*/
|
|
63
|
+
export const DEFAULT_BUILTIN_TOOLS = [...EXECUTABLE_BUILTINS];
|
|
60
64
|
/** `read` → `Read`: the vocabulary is small enough to forgive casing. */
|
|
61
65
|
const CANONICAL_BUILTIN = new Map([...EXECUTABLE_BUILTINS].map((name) => [name.toLowerCase(), name]));
|
|
62
66
|
/**
|
|
@@ -78,15 +82,17 @@ export function parseBuiltinTools(value) {
|
|
|
78
82
|
}
|
|
79
83
|
/**
|
|
80
84
|
* Parse `BIR_BASH_ALLOW`: comma-separated command prefixes, `*` for
|
|
81
|
-
* everything.
|
|
82
|
-
*
|
|
85
|
+
* everything. Unset means everything, because `Bash` is on the default list.
|
|
86
|
+
* Set, it is the whole list, as `BIR_BUILTIN_TOOLS` is: `none` (or an empty
|
|
87
|
+
* value) allows no command.
|
|
83
88
|
*/
|
|
84
89
|
export function parseBashAllow(value) {
|
|
85
|
-
|
|
90
|
+
if (value === undefined)
|
|
91
|
+
return ["*"];
|
|
92
|
+
return value
|
|
86
93
|
.split(",")
|
|
87
94
|
.map((s) => s.trim())
|
|
88
|
-
.filter(
|
|
89
|
-
return prefixes.length > 0 ? prefixes : undefined;
|
|
95
|
+
.filter((s) => s.length > 0 && !["none", "off", "0"].includes(s.toLowerCase()));
|
|
90
96
|
}
|
|
91
97
|
/**
|
|
92
98
|
* Whether `command` matches the `BIR_BASH_ALLOW` prefixes. A prefix that ends
|
|
@@ -485,13 +491,24 @@ async function runBash(input, ctx) {
|
|
|
485
491
|
if (typeof command !== "string" || command.trim() === "") {
|
|
486
492
|
throw new BuiltinNotRunnable("command is required");
|
|
487
493
|
}
|
|
488
|
-
if (!ctx.bashAllow) {
|
|
489
|
-
throw new BuiltinNotRunnable("BIR_BASH_ALLOW
|
|
494
|
+
if (!ctx.bashAllow?.length) {
|
|
495
|
+
throw new BuiltinNotRunnable("BIR_BASH_ALLOW allows no command, so bir runs no Bash command unattended");
|
|
490
496
|
}
|
|
491
497
|
if (!bashAllowed(command, ctx.bashAllow)) {
|
|
492
498
|
throw new BuiltinNotRunnable("the command does not match BIR_BASH_ALLOW, so bir does not run it unattended");
|
|
493
499
|
}
|
|
500
|
+
// What the host would run differently stays the session's, never started
|
|
501
|
+
// here: a command it leaves in the background (there is no shell here to
|
|
502
|
+
// leave it in, so it would be stopped with the step), and one that asks for
|
|
503
|
+
// more time than this step has. Cut off part-way, an `npm i -g` or a
|
|
504
|
+
// migration can leave things half done, which is worse than a hand-over.
|
|
505
|
+
if (input.run_in_background === true) {
|
|
506
|
+
throw new BuiltinNotRunnable("the command runs in the background, which only the session can do");
|
|
507
|
+
}
|
|
494
508
|
const timeoutMs = ctx.timeoutMs ?? 120_000;
|
|
509
|
+
if (typeof input.timeout === "number" && input.timeout > timeoutMs) {
|
|
510
|
+
throw new BuiltinNotRunnable(`the command asks for up to ${Math.ceil(input.timeout / 1000)} s and this step has ${Math.floor(timeoutMs / 1000)} s, so the session runs it`);
|
|
511
|
+
}
|
|
495
512
|
// The host's Bash semantics, which the recorded chain was written against:
|
|
496
513
|
// the working directory carries over from the plan's previous Bash step,
|
|
497
514
|
// variables and functions do not (a fresh `-l` shell each call).
|
|
@@ -191,17 +191,17 @@ export interface ReplayOptions {
|
|
|
191
191
|
* `BIR_BUILTIN_TOOLS` — the built-ins the runner may execute itself, from
|
|
192
192
|
* the session's own project directory (replay/builtins.ts), or stand in
|
|
193
193
|
* for with a recorded output under `BIR_REPLAY_RECORDED_OUTPUTS=1`.
|
|
194
|
-
* Omitted, the environment decides; the default is
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
* {@link ReplayOptions.bashAllow}.
|
|
194
|
+
* Omitted, the environment decides; the default is all six (`Read`,
|
|
195
|
+
* `Glob`, `Grep`, `Edit`, `Write`, `Bash`), and `Bash` runs only the
|
|
196
|
+
* commands {@link ReplayOptions.bashAllow} allows.
|
|
198
197
|
*/
|
|
199
198
|
builtinTools?: ReadonlySet<string>;
|
|
200
199
|
/**
|
|
201
200
|
* `BIR_BASH_ALLOW` — command prefixes a runner-side `Bash` step may run
|
|
202
|
-
* (`*` for all).
|
|
203
|
-
*
|
|
204
|
-
*
|
|
201
|
+
* (`*` for all). Omitted, the environment decides; the default is `*`.
|
|
202
|
+
* Empty allows no command, and then `Bash` counts as off
|
|
203
|
+
* {@link ReplayOptions.builtinTools}: its steps are steered in the session,
|
|
204
|
+
* rather than armed `direct` and stopped at the first one.
|
|
205
205
|
*/
|
|
206
206
|
bashAllow?: readonly string[];
|
|
207
207
|
budgets?: Partial<ReplayBudgets>;
|
|
@@ -475,7 +475,7 @@ export declare class ReplayController {
|
|
|
475
475
|
/** See {@link ReplayOptions.builtinTools}. */
|
|
476
476
|
private readonly builtinTools;
|
|
477
477
|
/** See {@link ReplayOptions.bashAllow}. */
|
|
478
|
-
private readonly bashAllow
|
|
478
|
+
private readonly bashAllow;
|
|
479
479
|
/** Calls waiting for a plan's pins to change (see `awaitPins`). */
|
|
480
480
|
private readonly pinWaiters;
|
|
481
481
|
/**
|
|
@@ -250,8 +250,14 @@ export class ReplayController {
|
|
|
250
250
|
this.intentMatch = { ...DEFAULT_INTENT_MATCH, ...(opts.intentMatch ?? {}) };
|
|
251
251
|
this.recordedFallback =
|
|
252
252
|
opts.recordedFallback ?? process.env.BIR_REPLAY_RECORDED_OUTPUTS === "1";
|
|
253
|
-
this.builtinTools = opts.builtinTools ?? parseBuiltinTools(process.env.BIR_BUILTIN_TOOLS);
|
|
254
253
|
this.bashAllow = opts.bashAllow ?? parseBashAllow(process.env.BIR_BASH_ALLOW);
|
|
254
|
+
const builtins = opts.builtinTools ?? parseBuiltinTools(process.env.BIR_BUILTIN_TOOLS);
|
|
255
|
+
// A `Bash` that may run no command is not reachable here. Left on the
|
|
256
|
+
// list, a chain of Bash steps would arm `direct` and stop at its first.
|
|
257
|
+
this.builtinTools =
|
|
258
|
+
this.bashAllow.length > 0 || !builtins.has("Bash")
|
|
259
|
+
? builtins
|
|
260
|
+
: new Set([...builtins].filter((name) => name !== "Bash"));
|
|
255
261
|
}
|
|
256
262
|
/**
|
|
257
263
|
* Whether a handed-out segment may actually arm (segmented.md R-OUT-10).
|
|
@@ -2067,10 +2073,10 @@ export class ReplayController {
|
|
|
2067
2073
|
// An allow-listed built-in runs *here*, against the session's own
|
|
2068
2074
|
// project directory (replay/builtins.ts). Everything else about it —
|
|
2069
2075
|
// no session cwd (`bir replay`, an older server), off the list, no
|
|
2070
|
-
// runner-side implementation, a path that leaves the project, `Bash`
|
|
2071
|
-
//
|
|
2072
|
-
// the session can do the step itself or its recorded
|
|
2073
|
-
// stand in (`BIR_REPLAY_RECORDED_OUTPUTS=1`).
|
|
2076
|
+
// runner-side implementation, a path that leaves the project, a `Bash`
|
|
2077
|
+
// command `BIR_BASH_ALLOW` does not allow — stays the old contract:
|
|
2078
|
+
// `not_run`, so the session can do the step itself or its recorded
|
|
2079
|
+
// output can stand in (`BIR_REPLAY_RECORDED_OUTPUTS=1`).
|
|
2074
2080
|
if (this.builtinTools.has(name) && state.sessionCwd) {
|
|
2075
2081
|
return this.executeBuiltin(name, input, state, opts.deadline);
|
|
2076
2082
|
}
|
|
@@ -8,14 +8,15 @@
|
|
|
8
8
|
* only happen inside the live session.
|
|
9
9
|
*
|
|
10
10
|
* `live` covers three genuinely different cases and treats them identically,
|
|
11
|
-
* correctly: a built-in
|
|
12
|
-
*
|
|
13
|
-
* — which is `scope: "dynamic"`, appears in no config
|
|
14
|
-
* never be wrapped at all.
|
|
11
|
+
* correctly: a built-in outside the built-in allow-list (`WebFetch`, `Task`,
|
|
12
|
+
* or one the operator took off it), an MCP server the user chose not to wrap,
|
|
13
|
+
* and `claude-in-chrome` — which is `scope: "dynamic"`, appears in no config
|
|
14
|
+
* file, and therefore can never be wrapped at all.
|
|
15
15
|
*
|
|
16
|
-
* A built-in *on* the allow-list (`BIR_BUILTIN_TOOLS`, default
|
|
17
|
-
*
|
|
18
|
-
* under `BIR_REPLAY_RECORDED_OUTPUTS=1` — serves
|
|
16
|
+
* A built-in *on* the allow-list (`BIR_BUILTIN_TOOLS`, by default all six the
|
|
17
|
+
* runner implements) is `direct`: the runner executes it itself
|
|
18
|
+
* (replay/builtins.ts), or — under `BIR_REPLAY_RECORDED_OUTPUTS=1` — serves
|
|
19
|
+
* its recorded output. The
|
|
19
20
|
* callers that ask a *different* question, "will a proxy report this call"
|
|
20
21
|
* (`ReplayController.reachFor`), deliberately pass no allow-list: a built-in
|
|
21
22
|
* is never proxied, whoever executes it.
|
package/dist/replay/coverage.js
CHANGED
|
@@ -8,14 +8,15 @@
|
|
|
8
8
|
* only happen inside the live session.
|
|
9
9
|
*
|
|
10
10
|
* `live` covers three genuinely different cases and treats them identically,
|
|
11
|
-
* correctly: a built-in
|
|
12
|
-
*
|
|
13
|
-
* — which is `scope: "dynamic"`, appears in no config
|
|
14
|
-
* never be wrapped at all.
|
|
11
|
+
* correctly: a built-in outside the built-in allow-list (`WebFetch`, `Task`,
|
|
12
|
+
* or one the operator took off it), an MCP server the user chose not to wrap,
|
|
13
|
+
* and `claude-in-chrome` — which is `scope: "dynamic"`, appears in no config
|
|
14
|
+
* file, and therefore can never be wrapped at all.
|
|
15
15
|
*
|
|
16
|
-
* A built-in *on* the allow-list (`BIR_BUILTIN_TOOLS`, default
|
|
17
|
-
*
|
|
18
|
-
* under `BIR_REPLAY_RECORDED_OUTPUTS=1` — serves
|
|
16
|
+
* A built-in *on* the allow-list (`BIR_BUILTIN_TOOLS`, by default all six the
|
|
17
|
+
* runner implements) is `direct`: the runner executes it itself
|
|
18
|
+
* (replay/builtins.ts), or — under `BIR_REPLAY_RECORDED_OUTPUTS=1` — serves
|
|
19
|
+
* its recorded output. The
|
|
19
20
|
* callers that ask a *different* question, "will a proxy report this call"
|
|
20
21
|
* (`ReplayController.reachFor`), deliberately pass no allow-list: a built-in
|
|
21
22
|
* is never proxied, whoever executes it.
|
package/docs/calculatedReplay.md
CHANGED
|
@@ -45,7 +45,7 @@ this design has a ladder whose cheapest rung is free — and the more of a stack
|
|
|
45
45
|
|---|---|---|
|
|
46
46
|
| Runs MCP tools itself | No (`replay.md` §12: *"rrepeat never executes tools — not even MCP"*) | **Yes**, on the connection it already owns |
|
|
47
47
|
| Divergence fallback | A second Agent SDK session — the dominant cost | Direct `tools/call` through the proxies — **$0** |
|
|
48
|
-
| Reaches built-ins (`Bash`, `Read`) | Only by steering the live session |
|
|
48
|
+
| Reaches built-ins (`Bash`, `Read`) | Only by steering the live session | `Read`, `Glob`, `Grep`, `Edit`, `Write` and `Bash` run on the runner itself, jailed to the project (`BIR_BUILTIN_TOOLS`, `BIR_BASH_ALLOW`); any other built-in is steered |
|
|
49
49
|
| Needs `@anthropic-ai/*` at runtime | Yes | No (§16.3) |
|
|
50
50
|
|
|
51
51
|
---
|
|
@@ -252,21 +252,26 @@ export function modeFor(steps: SerializedScenarioStep[], wrapped: ReadonlySet<st
|
|
|
252
252
|
| — | `none` | nowhere | declines at gate 5 |
|
|
253
253
|
|
|
254
254
|
`live` covers three genuinely different cases and treats them identically, correctly: a built-in
|
|
255
|
-
off the `BIR_BUILTIN_TOOLS` allow-list (by default everything but
|
|
256
|
-
`
|
|
255
|
+
off the `BIR_BUILTIN_TOOLS` allow-list (by default everything but the six the runner implements —
|
|
256
|
+
so `Task`, `WebFetch`; up to 0.3.7 also `Bash`, `Edit`, `Write`), an MCP server the user chose not to wrap, and
|
|
257
257
|
`claude-in-chrome` — which is `scope: "dynamic"`, appears in no config file, and therefore can never
|
|
258
258
|
be wrapped (v1 design §1). None of them can be reached except through the session.
|
|
259
259
|
|
|
260
260
|
A built-in **on** the allow-list runs on the runner, in the session's own project directory and
|
|
261
261
|
confined to it — a path that resolves outside the project, symlinks included, is refused and the
|
|
262
|
-
step is `not_run`, the clean hand-over. `Bash`
|
|
263
|
-
command prefixes
|
|
262
|
+
step is `not_run`, the clean hand-over. `Bash` has a second switch on top (`BIR_BASH_ALLOW`,
|
|
263
|
+
command prefixes; unset means any command, `none` keeps `Bash` in the session), because a
|
|
264
|
+
runner-side step never reaches the host's permission prompts. The
|
|
264
265
|
results mimic the hook's view of each tool, which is what the recorder stored for built-in steps
|
|
265
266
|
and what their `toolOutputLogic` was authored against; they pass through the same
|
|
266
267
|
`serializeCapped(redact(…))` as every recorded payload. Bash keeps the host's own semantics a
|
|
267
268
|
recorded chain was written against: each call is a fresh `-l` shell (variables and functions do
|
|
268
269
|
not persist), while the **working directory carries over** from the plan's previous Bash step —
|
|
269
|
-
`cd sub` then `cat file` replays as it ran.
|
|
270
|
+
`cd sub` then `cat file` replays as it ran. Two Bash calls the host runs differently are never
|
|
271
|
+
started on the runner, and are `not_run` instead: one with `run_in_background` (there is no shell
|
|
272
|
+
here to leave it in), and one whose own `timeout` is longer than the step's clock
|
|
273
|
+
(`BIR_STEP_TIMEOUT_MS`, clipped to what the plan has left) — cut off part-way, an `npm i -g` or a
|
|
274
|
+
migration can leave things half done.
|
|
270
275
|
|
|
271
276
|
A **mixed** scenario in `steer` mode still gets direct execution for its wrapped steps once it
|
|
272
277
|
diverges (§8), so coverage is not all-or-nothing at the divergence boundary — only at the arm-time
|
|
@@ -234,7 +234,10 @@ When replay arms:
|
|
|
234
234
|
when it plausibly *is* the step — the same command, the same file, an argument in common (design
|
|
235
235
|
§7) — and never in plan mode.
|
|
236
236
|
- **Direct steps are not visible to the permission system at all.** They are dispatched straight to a
|
|
237
|
-
`bir-proxy`, which calls the upstream on a connection it already holds
|
|
237
|
+
`bir-proxy`, which calls the upstream on a connection it already holds — or, for Claude Code's own
|
|
238
|
+
`Read`, `Glob`, `Grep`, `Edit`, `Write` and `Bash`, run by the recorder itself in the project
|
|
239
|
+
folder, any `Bash` command included (`BIR_BUILTIN_TOOLS`, `BIR_BASH_ALLOW`; up to 0.3.7 only the
|
|
240
|
+
first three). Claude Code never sees them.
|
|
238
241
|
The `run_scenario` call that starts them is answered `allow` too, while the runner has announced a
|
|
239
242
|
direct plan; your own deny or ask rules for it still apply.
|
|
240
243
|
|
|
@@ -243,7 +246,9 @@ That is what makes replay fast, and it is the whole risk. Two things follow:
|
|
|
243
246
|
1. Only enable it in a project whose scenarios you have dry-replayed and read (§3.1).
|
|
244
247
|
2. Run `bir replay allow <servers>` (§5.3). An allowlist of the servers you are comfortable having
|
|
245
248
|
called unattended is cheap insurance, and it is the difference between "replay reads a dashboard"
|
|
246
|
-
and "replay can do anything any wrapped server can do".
|
|
249
|
+
and "replay can do anything any wrapped server can do". The same goes for built-ins:
|
|
250
|
+
`BIR_BUILTIN_TOOLS=Read,Glob,Grep` keeps writes and commands with the model, and
|
|
251
|
+
`BIR_BASH_ALLOW=git,npm test` (for example) limits the commands the recorder runs itself.
|
|
247
252
|
|
|
248
253
|
Nothing else is approved on your behalf. Outside a matched scenario's own steps, a wrapped MCP call
|
|
249
254
|
keeps its permission prompt, as it would without the runner (`bir doctor` says so in a note);
|
|
@@ -354,8 +359,8 @@ so there is exactly one switch and it cannot get out of step with itself.
|
|
|
354
359
|
| `BIR_STEP_TIMEOUT_MS` | `60000` | Per-step ceiling for one direct `tools/call`, clipped to what the plan has left |
|
|
355
360
|
| `BIR_LOGIC_TIMEOUT_MS` | `1000` | How long one scenario logic body may run before it is stopped. At most 10 000 |
|
|
356
361
|
| `BIR_REPLAY_RECORDED_OUTPUTS` | *(unset)* | `1` lets a step that could not be sent (a built-in off `BIR_BUILTIN_TOOLS`, no proxy, a server off the allowlist) stand on its recorded output instead of stopping the plan. Never for a timeout or an upstream error |
|
|
357
|
-
| `BIR_BUILTIN_TOOLS` | `Read,Glob,Grep` | Built-ins the runner executes itself during a replay, in the session's own project directory and confined to it. A chain whose only non-MCP steps are on this list arms `direct`.
|
|
358
|
-
| `BIR_BASH_ALLOW` | *(unset)* | Command prefixes a runner-side `Bash` step may run (`*` for all). Unset
|
|
362
|
+
| `BIR_BUILTIN_TOOLS` | `Read,Glob,Grep,Edit,Write,Bash` | Built-ins the runner executes itself during a replay, in the session's own project directory and confined to it. A chain whose only non-MCP steps are on this list arms `direct`. A runner-side step never reaches the host's permission prompts, so leave off any tool you want the model to call itself; `Read,Glob,Grep` is the read-only set (the default up to 0.3.7). `none`: every built-in step is steered in the session |
|
|
363
|
+
| `BIR_BASH_ALLOW` | *(unset: any command)* | Command prefixes a runner-side `Bash` step may run (`*` for all). Unset means any command; `none` means no command, and `Bash` steps are steered in the session. A plan's Bash steps share a working directory, exactly as the host's own Bash tool does — variables and functions do not persist, also as the host's. A call with `run_in_background`, or with its own `timeout` longer than the step's clock (`BIR_STEP_TIMEOUT_MS`), is never started on the runner: the plan hands over in front of it |
|
|
359
364
|
| `BIR_INTENT_ARM` | *(unset)* | `1` lets a match found mid-turn arm a plan in the calling tool's place, which means waiting for it (`BIR_INTENT_ARM_BUDGET_MS`, 500); `0` never waits; unset follows `BIR_SEGMENT_ARM` |
|
|
360
365
|
| `BIR_VERBOSE` | *(unset)* | `1` adds per-step `replay.*` detail lines |
|
|
361
366
|
|
|
@@ -553,7 +558,7 @@ matches. A prompt that missed both is matched once more on its task line (`PROMP
|
|
|
553
558
|
| `replay.vetoed … the model's first call was …, not run_scenario` | A direct plan, and the model reached for another tool first — usually because it has no `run_scenario` | As for `no_delivery_tool`. Nothing ran; the model did the task |
|
|
554
559
|
| `replay.pin_declined` on most steered turns | The model's calls to the expected tool are not the step — another command, another file — so nothing is pinned; if it then moves on, `replay.pin_abandoned` retires the plan | Harmless for those calls, which ran as the model wrote them. Persistent declines on one step usually mean its arguments cannot be shown in the directive (they depend on an earlier step's output, or the command runs to several lines), and the model writes its own |
|
|
555
560
|
| `why="no step is executable"` | None of the scenario's tools is a wrapped MCP server and none is a built-in reachable in this session | Wrap the servers the scenario uses (`bir install --server …`) and recalculate |
|
|
556
|
-
| `plan.armed mode=steer` where you expected `direct` | At least one step is a built-in off `BIR_BUILTIN_TOOLS` (default
|
|
561
|
+
| `plan.armed mode=steer` where you expected `direct` | At least one step is a built-in off `BIR_BUILTIN_TOOLS` (by default one the runner does not implement, such as `WebFetch` or `Task`; a `Bash` step too when `BIR_BASH_ALLOW=none`), an unwrapped MCP server, or `claude-in-chrome`. Before 0.3.8 the default list was `Read,Glob,Grep`, so `Edit`, `Write` and `Bash` steered too | `bir status` shows what is wrapped; the `plan.armed` line's `coverage=` names the `live` step. `claude-in-chrome` is `scope: "dynamic"` and can never be wrapped |
|
|
557
562
|
| Model ignores the directive and diverges every turn | Steering is advisory — the model chooses; `bir` only pins arguments | Expected occasionally. Persistent divergence usually means the scenario's tools do not fit the live task; check `intent` |
|
|
558
563
|
| A `replay.thread` line with no `emitted=` (under `BIR_VERBOSE=1`) | `toolOutputLogic` derived nothing — usually the output shape changed since recording | Dry-replay (§3.1). If the dry run emits and the live run does not, the tool's output shape has drifted; recalculate |
|
|
559
564
|
| `replay.thread_fallback` on a wrapped step | The proxy did not report within the grace window | Check that proxy is alive (`bir doctor`). Repeats mean a slow or dying upstream |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@basein/runner",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.8",
|
|
4
4
|
"description": "A recording MCP proxy: sits between any MCP client and its MCP servers, executes each call on the client's behalf, and records the run as a reusable BaseIn scenario.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/production.md
CHANGED
|
@@ -991,8 +991,8 @@ Defaults were checked in `src/`.
|
|
|
991
991
|
| `BIR_STEP_TIMEOUT_MS` | recorder | 60000 | One direct `tools/call`. |
|
|
992
992
|
| `BIR_LOGIC_TIMEOUT_MS` | recorder | 1000 (capped at 10000) | Synchronous time limit for one scenario logic body. |
|
|
993
993
|
| `BIR_REPLAY_RECORDED_OUTPUTS` | recorder | unset | `1`: a step that could not be sent may use its recorded output, labelled as such. |
|
|
994
|
-
| `BIR_BUILTIN_TOOLS` | recorder | `Read,Glob,Grep` | Built-ins the runner executes itself during a replay, in the session's project directory and confined to it.
|
|
995
|
-
| `BIR_BASH_ALLOW` | recorder | unset | Command prefixes a runner-side `Bash` step may run (`*` for all). Unset
|
|
994
|
+
| `BIR_BUILTIN_TOOLS` | recorder | `Read,Glob,Grep,Edit,Write,Bash` | Built-ins the runner executes itself during a replay, in the session's project directory and confined to it (up to 0.3.7 the default was `Read,Glob,Grep`). Runner-side steps bypass the host's permission prompts; leave a tool off to have the model call it. `none` turns it off. |
|
|
995
|
+
| `BIR_BASH_ALLOW` | recorder | unset (any command) | Command prefixes a runner-side `Bash` step may run (`*` for all). Unset means any command; `none` means no command, and `Bash` steps are steered. A plan's Bash steps share a working directory, as the host's do; variables do not persist, also as the host's. |
|
|
996
996
|
| `BIR_DERIVE_RECENT_RESULTS` | recorder | 5 | How many of this prompt's tool results derivation may read (`0`: none). Never for plans with a command sink. |
|
|
997
997
|
| `BIR_INTENT_MATCH` | recorder | on | `0` turns mid-turn intent probes off. |
|
|
998
998
|
| `BIR_INTENT_MATCH_BUDGET_MS` | recorder | 4000 | One probe's own ceiling. It is not a hook wait. |
|