@basein/runner 0.3.19 → 0.3.21

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 CHANGED
@@ -360,13 +360,13 @@ same command, the same file, the same kind of arguments); any other call runs as
360
360
  written and is asked about as usual. A *directly executed* step never reaches the
361
361
  permission system at all — it is dispatched to a `bir-proxy` and called on a
362
362
  connection that is already open, or, for Claude Code's own `Read`, `Glob`,
363
- `Grep`, `Edit`, `Write` and `Bash`, run by the recorder itself in the project
364
- folder — and `run_scenario`, the call that starts the
363
+ `Grep`, `Edit`, `Write` and `Bash`, run by the recorder itself from the project
364
+ folder, on any file the agent could reach — and `run_scenario`, the call that starts the
365
365
  run, is answered `allow` while the recorder has announced the plan. In plan mode
366
366
  nothing is announced and nothing is approved. Set `BIR_REPLAY_ALLOW_SERVERS` to
367
367
  the servers you are content to have called unattended (it also covers a steered
368
- plan's recovery), `BIR_BUILTIN_TOOLS` and `BIR_BASH_ALLOW` to the built-ins and
369
- commands, and read [the guide's §5.1](docs/calculatedReplayGuide.md)
368
+ plan's recovery), `BIR_BUILTIN_TOOLS`, `BIR_PATH_ALLOW` and `BIR_BASH_ALLOW` to
369
+ the built-ins, folders and commands, and read [the guide's §5.1](docs/calculatedReplayGuide.md)
370
370
  before enabling it anywhere that matters.
371
371
 
372
372
  What it buys: on a scenario whose every step is a wrapped MCP tool or one of
@@ -489,8 +489,9 @@ what to do when they register, so there is one switch and no second copy of it:
489
489
  |---|---|
490
490
  | `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. |
491
491
  | `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. |
492
- | `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. |
492
+ | `BIR_BUILTIN_TOOLS` | Built-in tools bir may execute itself during a replay — from the session's own project directory; which folders the file tools may touch is `BIR_PATH_ALLOW`. 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. |
493
493
  | `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). |
494
+ | `BIR_PATH_ALLOW` | Folders a runner-side `Read`, `Glob`, `Grep`, `Edit` or `Write` may touch. Unset means any folder (`*`), as the agent's own tools: a plan that reads the agent's memory file under `~/.claude` runs (from 0.3.21; up to 0.3.20 only the project directory). `project` keeps them inside the session's project directory; add folders after a comma, `~` for your home folder: `project,~/.claude`. `none` allows no folder, and file steps are then steered in the session. |
494
495
  | `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. |
495
496
  | `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. |
496
497
  | `BIR_DERIVE_MODEL` | Derivation model, when you set a key of your own (default `claude-haiku-4-5-20251001`). |
@@ -62,9 +62,9 @@
62
62
  * BIR_REPLAY_ALLOW_SERVERS comma-separated server keys eligible for *direct*
63
63
  * execution. Unset means every wrapped server
64
64
  * BIR_BUILTIN_TOOLS comma-separated built-ins the runner may execute
65
- * itself during a replay, in the session's own
66
- * project directory and confined to it
67
- * (replay/builtins.ts). Default all six:
65
+ * itself during a replay, from the session's own
66
+ * project directory (replay/builtins.ts; which
67
+ * folders: BIR_PATH_ALLOW). Default all six:
68
68
  * Read,Glob,Grep,Edit,Write,Bash. A runner-side
69
69
  * step never reaches the host's permission
70
70
  * prompts; Read,Glob,Grep is the read-only set
@@ -73,6 +73,13 @@
73
73
  * BIR_BASH_ALLOW command prefixes a runner-side Bash step may run
74
74
  * (`*` for all). Unset means `*`; `none` means no
75
75
  * command, and Bash steps are steered
76
+ * BIR_PATH_ALLOW folders a runner-side Read, Glob, Grep, Edit or
77
+ * Write may touch (`*` for any). Unset means `*`:
78
+ * any path, as the agent's own tools (up to 0.3.20
79
+ * the project directory alone). `project` is the
80
+ * session's project directory, `~` the home
81
+ * folder: `project,~/.claude`. `none` means no
82
+ * folder, and the file steps are steered
76
83
  * BIR_DERIVE_API_KEY OPTIONAL. Derivation — reading what this turn acts
77
84
  * on — is done by the service for a signed-in
78
85
  * runner. Set this (an Anthropic key) only to keep
@@ -62,9 +62,9 @@
62
62
  * BIR_REPLAY_ALLOW_SERVERS comma-separated server keys eligible for *direct*
63
63
  * execution. Unset means every wrapped server
64
64
  * BIR_BUILTIN_TOOLS comma-separated built-ins the runner may execute
65
- * itself during a replay, in the session's own
66
- * project directory and confined to it
67
- * (replay/builtins.ts). Default all six:
65
+ * itself during a replay, from the session's own
66
+ * project directory (replay/builtins.ts; which
67
+ * folders: BIR_PATH_ALLOW). Default all six:
68
68
  * Read,Glob,Grep,Edit,Write,Bash. A runner-side
69
69
  * step never reaches the host's permission
70
70
  * prompts; Read,Glob,Grep is the read-only set
@@ -73,6 +73,13 @@
73
73
  * BIR_BASH_ALLOW command prefixes a runner-side Bash step may run
74
74
  * (`*` for all). Unset means `*`; `none` means no
75
75
  * command, and Bash steps are steered
76
+ * BIR_PATH_ALLOW folders a runner-side Read, Glob, Grep, Edit or
77
+ * Write may touch (`*` for any). Unset means `*`:
78
+ * any path, as the agent's own tools (up to 0.3.20
79
+ * the project directory alone). `project` is the
80
+ * session's project directory, `~` the home
81
+ * folder: `project,~/.claude`. `none` means no
82
+ * folder, and the file steps are steered
76
83
  * BIR_DERIVE_API_KEY OPTIONAL. Derivation — reading what this turn acts
77
84
  * on — is done by the service for a signed-in
78
85
  * runner. Set this (an Anthropic key) only to keep
@@ -246,6 +246,11 @@ export const DECLINES = {
246
246
  cause: "the plan was not ready before the hook that would have announced it had to answer: the prompt hook waits 7.5 s in all, match included, and a tool call that starts a plan mid-task waits 1.5 s. What arming waits for is the reading of the prompt's values (the derivation). A plan that was not announced is never run.",
247
247
  fix: "run the prompt again. If it keeps happening the reading is slow: the recorder's log (`bir status` names the file) shows when its `replay.derived` line landed, and a dry replay shows what the service's reading took, under `reading` (`bir scenario replay <scnId> --prompt \"…\" --dry --json`). BIR_HOOK_DERIVE_BUDGET_MS does not help here, because the hook's own limit was reached first. To read values on this machine instead of asking the service, set BIR_DERIVE_API_KEY where Claude Code starts.",
248
248
  },
249
+ // Nor this one: the control server's own rule (the service's offer1.md O8).
250
+ would_repeat: {
251
+ cause: "a plan the intent check matched would have started after the agent's call, not in its place, and its steps call that same tool: it would have made the call a second time — an e-mail sent twice (issue fx2). Never twice: it was not started, and the agent went on.",
252
+ fix: "nothing to fix in the plan. A scenario starts in the call's place only when the agent's call is its first step, value for value; one recorded in the order the agent works in (the agent's own run after a decision becomes one) is what starts there.",
253
+ },
249
254
  // Nor is this one: the match itself was late, so nothing was armed at all.
250
255
  match_late: {
251
256
  cause: "the service's answer to the match landed too late: after the prompt hook had stopped waiting for it (3 s, BIR_MATCH_BUDGET_MS); while prompts were not waiting on the match at all — after one slow answer they stop waiting for a few minutes (`replay.match_gate state=holding`); or with the hook not waiting from the start, because a failure to reach the service was still on record (a `service.degraded` line, perhaps hours before, and no `service.recovered` until after this prompt) or the match request's first attempt failed. The agent did the task itself.",
@@ -408,7 +413,7 @@ export function localFindings(turn) {
408
413
  const elsewhere = names.filter((n) => !EXECUTABLE_BUILTINS.has(n));
409
414
  const fixes = [];
410
415
  if (offList.length > 0) {
411
- 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\`.`);
416
+ 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\` (for the file tools, \`BIR_PATH_ALLOW\`): fix those where the recorder starts, then \`bir up --restart\`.`);
412
417
  }
413
418
  if (elsewhere.length > 0) {
414
419
  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.`);
@@ -26,7 +26,7 @@ import { DISCOVERY_CHECK_MS, HEARTBEAT_MS, pidAlive, readDiscovery, removeDiscov
26
26
  import { DAEMON_CWD_ENV, DAEMON_ENV, DAEMON_LOG_ENV, DERIVE_KEY_ENV, DaemonLog, USE_API_KEY_ENV, clearPortProblem, compareVersions, probeHealth, projectEndpoint, rediscover, validPort, writePortProblem, } from "./daemon.js";
27
27
  import { configDir, normalizePath, supersededLogPath } from "./paths.js";
28
28
  import { RecorderKeeper } from "./recorder-keeper.js";
29
- import { parseBashAllow, parseBuiltinTools } from "../replay/builtins.js";
29
+ import { FILE_BUILTINS, parseBashAllow, parseBuiltinTools, parsePathAllow } from "../replay/builtins.js";
30
30
  import { parseAllowList } from "../replay/coverage.js";
31
31
  import { sweepDeliveries } from "../replay/bash-channel.js";
32
32
  import { exemptLoopbackFromProxy, isWrapped, projectRecord, projectToken, readSidecar, } from "../config/generate.js";
@@ -197,15 +197,18 @@ function buildReplayOptions(keeper, policy, key) {
197
197
  : undefined;
198
198
  // Which built-ins the runner may execute itself (replay/builtins.ts):
199
199
  // all six by default, `BIR_BUILTIN_TOOLS` to narrow them, `BIR_BASH_ALLOW`
200
- // to narrow the commands a `Bash` step may run. Environment-only knobs,
201
- // like `BIR_REPLAY_RECORDED_OUTPUTS`.
200
+ // to narrow the commands a `Bash` step may run, `BIR_PATH_ALLOW` the
201
+ // folders a file step may touch (unset: any, as the agent's own tools).
202
+ // Environment-only knobs, like `BIR_REPLAY_RECORDED_OUTPUTS`.
202
203
  const builtinTools = parseBuiltinTools(process.env.BIR_BUILTIN_TOOLS);
203
204
  const bashAllow = parseBashAllow(process.env.BIR_BASH_ALLOW);
205
+ const pathAllow = parsePathAllow(process.env.BIR_PATH_ALLOW);
204
206
  const opts = {
205
207
  enabled,
206
208
  allowServers,
207
209
  builtinTools,
208
210
  bashAllow,
211
+ pathAllow,
209
212
  budgets: {
210
213
  // What a prompt may wait for its match (K5). Past it the prompt goes on
211
214
  // without one; a service slower than this must not cost every prompt.
@@ -263,6 +266,15 @@ function buildReplayOptions(keeper, policy, key) {
263
266
  // all — loud for the same reason as the rest of this line.
264
267
  builtins: builtinTools.size > 0 ? [...builtinTools].join(",") : "(none)",
265
268
  bashAllow: builtinTools.has("Bash") ? (bashAllow.length > 0 ? bashAllow.join(",") : "(none — Bash steps are steered)") : undefined,
269
+ // Where the file tools may reach: anywhere, as the agent's own, unless
270
+ // BIR_PATH_ALLOW names folders (up to 0.3.20 the project alone).
271
+ pathAllow: [...builtinTools].some((name) => FILE_BUILTINS.has(name))
272
+ ? pathAllow.includes("*")
273
+ ? "(any folder)"
274
+ : pathAllow.length > 0
275
+ ? pathAllow.join(",")
276
+ : "(none — file steps are steered)"
277
+ : undefined,
266
278
  // Who reads what this turn acts on (segmented.md R-PARAM-5). Named at
267
279
  // startup because the third state is a replay that silently never runs.
268
280
  derive: key.source ??
@@ -1202,6 +1202,70 @@ export declare class ControlServer {
1202
1202
  * computed is "no" — the directive then rides on the call's PostToolUse.
1203
1203
  */
1204
1204
  private isFirstStep;
1205
+ /**
1206
+ * The agent's call against `state`'s first step, value for value (the
1207
+ * service's offer1.md O5): `hit` when every argument is the step's own,
1208
+ * `missing` when the only differences are values the plan does not have
1209
+ * (a target the reading left empty, which the agent's call may hold), and
1210
+ * `other` otherwise.
1211
+ */
1212
+ private firstStepVerdict;
1213
+ /**
1214
+ * Whether `state`'s plan calls `toolName` in a step it would run: what makes
1215
+ * a plan started after a call of that tool make it a second time (the
1216
+ * service's offer1.md O8). A tool that only reads never counts — reading
1217
+ * twice changes nothing — by the service's own rule ({@link readsOnly}): a
1218
+ * `crm_search_customers` mid-task never keeps a plan that searches later from
1219
+ * starting.
1220
+ */
1221
+ private planCallsTool;
1222
+ /**
1223
+ * A plan of this turn stopped at its decision: ask the service, once per
1224
+ * plan, which calculated scenarios can follow it, and arm each in the
1225
+ * background while the agent reads the note and decides (offer1.md O3, O4).
1226
+ * Called wherever a hand-over is first seen; a second call for the same plan
1227
+ * does nothing.
1228
+ */
1229
+ private noteDecision;
1230
+ /**
1231
+ * Ask for a decision's next scenarios (offer1.md O3): each is checkable as
1232
+ * soon as the service answers, and armed in the background, each on its own
1233
+ * (O4). Resolves when the service has answered, not when the arms have.
1234
+ * Never rejects.
1235
+ */
1236
+ private fetchNext;
1237
+ /**
1238
+ * Arm one next scenario in the background (offer1.md O4): its values read
1239
+ * from the turn's request and its results so far — or, once more, with the
1240
+ * agent's own call (O5) — and settled, so a call is compared with the first
1241
+ * step as it would really run. Never rejects.
1242
+ */
1243
+ private armNext;
1244
+ /**
1245
+ * The agent's call after a decision, against the scenarios that can follow
1246
+ * it (offer1.md O5, O6). A hit — the call is a scenario's first step, value
1247
+ * for value — is answered in the call's place with that scenario's directive:
1248
+ * the agent calls `run_scenario`, and the scenario runs every step, this one
1249
+ * included, once. Undefined otherwise: the call runs as the agent wrote it.
1250
+ */
1251
+ private tryNextScenarios;
1252
+ /** A next scenario's hit: bookkeeping as for any plan armed mid-task, then its directive in the call's place. */
1253
+ private nextHit;
1254
+ /**
1255
+ * A next scenario hit after the agent's own calls — looks — opened the
1256
+ * fragment of its work after the decision: the fragment ends here, with what
1257
+ * the agent did itself, and nothing more of this turn is recorded into it.
1258
+ * The scenario's own calls, pinned in steer mode or run in direct mode, are
1259
+ * not the agent's work, and would make the fragment a copy of that scenario
1260
+ * (offer1.md O7). A fragment still due is dropped instead
1261
+ * ({@link dropDecisionFragment}).
1262
+ */
1263
+ private closeDecisionFragment;
1264
+ /**
1265
+ * End this turn's next scenarios (offer1.md O6): every one but the hit is
1266
+ * never announced, so never runs and is never reported.
1267
+ */
1268
+ private endNext;
1205
1269
  /**
1206
1270
  * Whether an intent probe may arm a plan in its own `PreToolUse` — and so
1207
1271
  * whether that hook waits for it. See {@link ControlServerOptions.intentArm}.