@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.
- package/README.md +41 -1
- package/dist/bin/bir-scenario.d.ts +29 -2
- package/dist/bin/bir-scenario.js +484 -16
- package/dist/bin/bir.d.ts +10 -0
- package/dist/bin/bir.js +143 -43
- package/dist/bin/investigate.js +2 -2
- package/dist/bin/scenario-edit.d.ts +200 -0
- package/dist/bin/scenario-edit.js +950 -0
- package/dist/config/generate.d.ts +21 -0
- package/dist/config/generate.js +16 -0
- package/dist/control/server.js +18 -5
- package/dist/record/housekeeping.d.ts +71 -0
- package/dist/record/housekeeping.js +417 -0
- package/dist/replay/controller.d.ts +3 -1
- package/dist/replay/controller.js +17 -12
- package/docs/calculatedReplay.md +115 -4
- package/docs/calculatedReplayGuide.md +528 -10
- package/docs/quickstart.md +44 -2
- package/package.json +1 -1
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* ignore. Nothing throws at a hook.
|
|
13
13
|
*/
|
|
14
14
|
import { parseQualifiedName } from "../control/correlation.js";
|
|
15
|
-
import {
|
|
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
|
/**
|
package/docs/calculatedReplay.md
CHANGED
|
@@ -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
|
|
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
|
|
1033
|
-
|
|
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`
|