@basein/runner 0.2.10 → 0.2.12

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.
@@ -12,7 +12,7 @@
12
12
  * ignore. Nothing throws at a hook.
13
13
  */
14
14
  import { parseQualifiedName } from "../control/correlation.js";
15
- import { isHousekeeping } from "../record/housekeeping.js";
15
+ import { isHousekeepingCall } from "../record/housekeeping.js";
16
16
  import { redact } from "../record/redact.js";
17
17
  import { serializeCapped } from "../record/truncate.js";
18
18
  import { logDetail, logLine, errText } from "../util/log.js";
@@ -392,7 +392,9 @@ export class ReplayController {
392
392
  * and hand it back through the best channel available; if even that fails,
393
393
  * abort to an ordinary turn.
394
394
  */
395
- async preTool(state, toolName, toolUseId) {
395
+ async preTool(state, toolName, toolUseId,
396
+ /** The call's `tool_input`: whether a `Bash` call is only `bir` is read from it (D13). */
397
+ toolInput) {
396
398
  const plan = state.plan;
397
399
  if (!plan || state.retired)
398
400
  return { kind: "passthrough" };
@@ -409,9 +411,21 @@ export class ReplayController {
409
411
  error: errText(err),
410
412
  });
411
413
  }
414
+ // HOUSEKEEPING IS NOT DIVERGENCE. See `record/housekeeping.ts`. Asked
415
+ // before the pin, not after it: a `Bash` step is pinned by name, and a
416
+ // `bir scenario show` the model runs while a Bash step is expected must
417
+ // run as `bir`, never be swapped for the plan's command (D13).
418
+ const expected = plan.expectedTool();
419
+ if (isHousekeepingCall(toolName, toolInput)) {
420
+ logDetail("replay.housekeeping", {
421
+ tool: toolName,
422
+ step: `${plan.currentStepIndex}/${plan.stepCount}`,
423
+ why: "host bookkeeping, not task work — plan stays armed",
424
+ });
425
+ return { kind: "passthrough" };
426
+ }
412
427
  // A direct plan does not steer individual calls: the model was asked for one
413
428
  // tool call and made a different one. That is divergence.
414
- const expected = plan.expectedTool();
415
429
  if (state.mode === "steer" && toolName === expected && toolUseId) {
416
430
  try {
417
431
  const step = plan.currentStep();
@@ -460,15 +474,6 @@ export class ReplayController {
460
474
  return { kind: "abort" };
461
475
  }
462
476
  }
463
- // HOUSEKEEPING IS NOT DIVERGENCE. See `record/housekeeping.ts`.
464
- if (isHousekeeping(toolName)) {
465
- logDetail("replay.housekeeping", {
466
- tool: toolName,
467
- step: `${plan.currentStepIndex}/${plan.stepCount}`,
468
- why: "host bookkeeping, not task work — plan stays armed",
469
- });
470
- return { kind: "passthrough" };
471
- }
472
477
  return await this.diverge(state, toolName, `expected ${expected ?? "no more tools"}, model called ${toolName}`);
473
478
  }
474
479
  /**
@@ -176,7 +176,7 @@ Three additions to the v1 topology, and nothing else moves:
176
176
  |---|---|---|
177
177
  | `bir-hooks` control server | extended | Holds the `ScenarioReplayPlan` on `RunState`. Arms it, pins inputs, threads outputs, dispatches direct steps, redeems the ticket |
178
178
  | `bir-proxy` | extended | Long-polls for work; executes a `tools/call` on the upstream it already owns |
179
- | `bir-scenario` | **new**, ~120 lines | A stdio MCP server exposing one tool, `run_scenario`. The delivery channel for direct mode (§6) |
179
+ | `bir-scenario` | **new**, ~120 lines | A stdio MCP server exposing one tool, `run_scenario`. The delivery channel for direct mode (§6). It later gained the scenario tools of §16.5 |
180
180
 
181
181
  ---
182
182
 
@@ -814,6 +814,49 @@ What actually constrains it:
814
814
  If that is not enough for a deployment, the mitigation is a real one and is out of scope here:
815
815
  evaluate the logic in a `node:vm` context with a frozen, minimal global. Filed as §20 question 3.
816
816
 
817
+ **A hand-edited step is the same code, and runs the same way.** Since 2026-09-25 (the service's
818
+ `editSteps.md`; built, not yet released) the owner — or Claude working for them — can change a step's
819
+ logic with `bir scenario edit`. The service checks the change against the recording before it saves
820
+ it, but that check is about *what the code computes*. Once saved, the step arrives in the same
821
+ payload and runs here unattended, with `new Function`, next to the same credentials, exactly like
822
+ calculated code. The provenance argument still holds: only the owner can edit, over their own token,
823
+ and anyone else — an admin included — gets `404`. What changes is that *"the model wrote it"* is no
824
+ longer always true.
825
+
826
+ The service sandboxes its own evaluation of every logic body (built with the same service change;
827
+ not yet released). It had to: there, one account's code runs next to every account's data and the
828
+ service's secrets. How it is built:
829
+
830
+ - **A fresh `node:vm` context for each evaluation**, with a time limit (`LOGIC_TIMEOUT_MS`). Only
831
+ JavaScript's own built-ins are in it, less `ArrayBuffer`, `SharedArrayBuffer`, `DataView`, the
832
+ typed arrays, `Atomics`, `WebAssembly` and `FinalizationRegistry`; `Symbol.for` and
833
+ `Symbol.keyFor` are removed too. No `require`, `process`, `fetch`, `Buffer` or timers. Values go
834
+ in and come out as JSON text only.
835
+ - **That context lives in a separate evaluator process**, not in the service: an empty
836
+ environment, `--permission` (no files, no child processes), `--disallow-code-generation-from-strings`,
837
+ `--frozen-intrinsics`, and a heap of `LOGIC_MEMORY_MB`.
838
+ - **A relay worker thread of the service drives that process.** No logic runs in the relay. It
839
+ kills the evaluator when a body runs too long, and the next job starts a new one; a body that
840
+ runs out of memory takes down only the evaluator, and is run once more on a fresh one before it
841
+ is blamed. The relay also replaces the evaluator when its heap stays above half of
842
+ `LOGIC_MEMORY_MB` after a job, and after 2,000 jobs. A body that overran its time or memory, or
843
+ took the evaluator down twice, is refused at once, with the same message, for the next 10
844
+ minutes.
845
+ - **The hand-edit check runs in a check worker thread of its own**, with its own relay and
846
+ evaluator process, under a sandbox-time budget
847
+ (`EDIT_CHECK_BUDGET_MS`, 5000 ms by default; past it, *too costly to check*), so a slow check
848
+ never holds up the service's other requests.
849
+
850
+ **The runner still does not sandbox.** Here the code runs on the owner's machine, for the owner's
851
+ account, with `new Function`. One consequence to know: **the service's check only judges what a
852
+ body returns for the recording's values.** Code that calls `fetch`, `process` or a timer on that
853
+ path, and does not catch the error, throws in the check, so it cannot be saved without `--force`.
854
+ But a call behind a `typeof` guard (`if (typeof fetch === "function") …`), or on a branch the
855
+ recording's values never take, is never reached in the check; one inside a `try` fails there
856
+ quietly. Such code passes, it is saved like any other change, and here it runs with `fetch`,
857
+ `process` and timers all present. The service's sandbox protects the service; it proves
858
+ nothing about what a body does on the runner. Read every hand edit before it is saved.
859
+
817
860
  ### 13.2 Replay bypasses permission prompts
818
861
 
819
862
  This is the sharpest edge in the whole design, and it must not be buried.
@@ -884,7 +927,7 @@ src/
884
927
  │ ├─ source-run.ts # lazy GET /recordings/runs/:id → recorded outputs by tool (§8.1)
885
928
  │ └─ types.ts # SerializedScenario, SerializedScenarioStep
886
929
  ├─ bin/
887
- │ ├─ bir-scenario.ts ← new: the one-tool MCP server (§6.3)
930
+ │ ├─ bir-scenario.ts ← new: the `bir` MCP server — `run_scenario` (§6.3), later the scenario tools (§16.5)
888
931
  │ └─ bir.ts # + `--replay`, `bir scenario …`, `bir replay` (§16.5)
889
932
  ├─ control/
890
933
  │ ├─ server.ts # + arm / pin / thread / dispatch / report
@@ -1029,8 +1072,8 @@ and a session with no BaseIn credentials still replays (it just books nothing).
1029
1072
 
1030
1073
  ### 16.5 CLI surface
1031
1074
 
1032
- `bir` gains one flag and one command group. Everything here is a thin client over endpoints that
1033
- already exist server-side, except `bir replay`, which drives §16.2's executor directly.
1075
+ `bir` gains one flag and one command group. Everything here is a thin client over service
1076
+ endpoints, except `bir replay`, which drives §16.2's executor directly.
1034
1077
 
1035
1078
  ```
1036
1079
  bir install --replay register the `bir` MCP server; raise the prompt-hook timeout to 15 s
@@ -1038,12 +1081,80 @@ bir uninstall --replay reverse exactly that, leaving the rest of the inst
1038
1081
 
1039
1082
  bir scenario list GET /recordings/runs runs, iterations, scenario state
1040
1083
  bir scenario show <runId> GET /recordings/runs/:id/scenario intent, params, steps, logic
1084
+ bir scenario show <scnId> GET /scenarios/:id the same, by scenario id (segments too)
1085
+ [--step <n>: one step, with scenarioId and chainRevision first]
1041
1086
  bir scenario calc <runId> POST /recordings/runs/:id/calculate [--force to re-derive in place]
1087
+ [--force --discard-edits: re-derive a plan with hand edits]
1088
+ bir scenario calc <scnId> --force
1089
+ POST /scenarios/:id/recalculate recalculate in place (segments too)
1090
+ [--discard-edits: sends {discardEdits: true}]
1042
1091
  bir scenario replay <scnId> --prompt "…" [--dry]
1043
1092
 
1093
+ bir scenario check <id> --step <n> POST /scenarios/:id/steps/:n/check try a change; writes nothing
1094
+ bir scenario edit <id> --step <n> PATCH /scenarios/:id/steps/:n the same check, then save
1095
+ bir scenario edits <id> GET /scenarios/:id/edits the history, newest first
1096
+ bir scenario undo <id> [--edit <e> | --step <n>]
1097
+ POST /scenarios/:id/edits/:e/revert take back the newest edit (of step n)
1098
+ bir scenario editing on|off|status (no route) ~/.baseinstrunner/installed.json which `bir` MCP tools are offered
1099
+
1044
1100
  bir replay --scenario <scnId> --prompt "…" [--dry] alias; the Tier 2 / debugging path (§12.1)
1045
1101
  ```
1046
1102
 
1103
+ The second group, and `show <scnId>`, `--step`, `calc <scnId>` and `--discard-edits`, were added
1104
+ on 2026-09-25 by the service's `editSteps.md`. They are built and tested, and not yet released in
1105
+ the form described here (version 0.2.11 on npm has an early form): they need the next
1106
+ `@basein/runner` version and a service deploy, because their routes are new on the service. They
1107
+ are where `bir` changes a scenario by hand, so the rules are strict:
1108
+
1109
+ - **Owner only.** Anyone else, an admin included, gets `404`, never `403`.
1110
+ - **A change is saved only if it reproduces the recording and is not a copy**, or on purpose with
1111
+ `--force --note "<why>"`. `check` and `edit` run the same check; `check` never writes. When the
1112
+ check had any problem, a forced save is recorded as forced, and `bir investigate` keeps saying so.
1113
+ - `<id>` is a `run_` id, resolved to its whole-run scenario with `GET /recordings/runs/:id/scenario`,
1114
+ or a `scn_` id, used as it is. A sub-task scenario has only its `scn_` id; for a run with only
1115
+ sub-task scenarios, `bir` lists their `scn_` ids. Logic goes in with `--input-logic` /
1116
+ `--output-logic` from a file, or `-` for stdin — never inline, because shell quoting mangles
1117
+ JavaScript. `edit` also takes `--freeze`, `--unfreeze`, `--note` and `--revision <n>` (the
1118
+ `expectedRevision` of the body).
1119
+ - `show --step <n>` prints the step object with `scenarioId` and `chainRevision` first. The revision
1120
+ is the scenario's, and is what `--revision` takes.
1121
+ - **`undo` walks back.** Without `--edit` it takes back the newest edit (never an undo entry) that
1122
+ can be undone; with `--step <n>`, the newest edit of step `n`. Run again, it goes one further
1123
+ back. `--edit` and `--step` cannot be given together. To redo, undo the undo entry by its
1124
+ `sedit_` id; the undo prints that command. The service checks an undo like a save, for the later
1125
+ steps and the final answer (so only when it changes output logic), and answers
1126
+ `422 undo_refused` when it would break them, unless `--force --note "<why>"` is given. Every
1127
+ save and every undo bumps the scenario's `chainRevision`.
1128
+ - A save lifts a mark made by hand (`--freeze`, `by_hand`) only with `--unfreeze`; undoing the
1129
+ freeze, or `calc --force --discard-edits`, also removes it. A forced save lifts only a mark the
1130
+ calculation set.
1131
+ - `calc --force` answers `409 scenario_has_edits` on a plan with hand edits, unless
1132
+ `--discard-edits` is given. `calc <scnId>` without `--force` is a usage error: nothing is sent.
1133
+ - Answers `bir` puts in words: `400 nothing_to_change` (the code and mark asked for are the step's
1134
+ already), `413 payload_too_large`, `503 logic_sandbox_unavailable` (nothing was checked, saved or
1135
+ undone), and a check report with `noChange: true` (`check` then says nothing would change, and
1136
+ exits `0`).
1137
+ - Exit codes: `0` checked OK, saved or undone; `1` refused, not found, or a service error; `2`
1138
+ usage. `--json` prints the service's body unchanged.
1139
+
1140
+ The same commands are tools on the `bir` MCP server. `scenario_show` (`target`, `step`),
1141
+ `scenario_edits` (`target`) and `investigate` (`id`) only read, and are always offered.
1142
+ `scenario_check` (`target`, `step`, `inputLogic`, `outputLogic`, `unfreeze`), `scenario_edit` (the
1143
+ same, plus `freeze`, `force`, `note`, `revision`) and `scenario_undo` (`target`, `edit` or `step`,
1144
+ `force`, `note`) are offered only in a project where `bir scenario editing on` was run, because that
1145
+ server runs in every session of every installed project. Each tool runs this package's own
1146
+ `bir … --json`, with logic bodies passed through temporary files; a refusal comes back as an MCP
1147
+ error result carrying the report. A call the host cancels, or leaves behind when it exits, has its
1148
+ `bir` stopped and its files removed.
1149
+
1150
+ **`bir`'s own calls are never recorded** (the service's `editSteps.md` D13; not yet released). The
1151
+ `bir` server's tools other than `run_scenario`, and a `Bash` or `PowerShell` command made only of
1152
+ `bir` invocations (and a plain `cd`), are host housekeeping, like `ToolSearch`
1153
+ ([housekeeping.ts](../src/record/housekeeping.ts)). They are never recorded as steps, and never
1154
+ count as divergence from a plan. A session that fixes a plan must not become a plan that edits
1155
+ plans when it is replayed. The operator's walk-through is
1156
+ [calculatedReplayGuide.md](calculatedReplayGuide.md) §9.2.
1157
+
1047
1158
  `--dry` maps to BaseIn's `POST /scenarios/:id/replay`, which evaluates the stored logic against the
1048
1159
  source run's *recorded* outputs — no real tools, no side effects, one Haiku call. Without `--dry`,
1049
1160
  `bir replay` runs the same plan through the same executor against the live proxies. `bir doctor`