@basein/runner 0.2.8 → 0.2.11
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 +86 -22
- package/dist/auth/client.d.ts +40 -1
- package/dist/auth/client.js +77 -9
- package/dist/bin/bir-hooks.d.ts +18 -3
- package/dist/bin/bir-hooks.js +124 -38
- package/dist/bin/bir-scenario.d.ts +18 -2
- package/dist/bin/bir-scenario.js +374 -4
- package/dist/bin/bir.d.ts +12 -0
- package/dist/bin/bir.js +501 -81
- package/dist/bin/investigate.js +1 -1
- package/dist/bin/scenario-edit.d.ts +173 -0
- package/dist/bin/scenario-edit.js +771 -0
- package/dist/bin/setup.d.ts +72 -0
- package/dist/bin/setup.js +286 -0
- package/dist/config/adapters/claude-code.d.ts +90 -4
- package/dist/config/adapters/claude-code.js +164 -16
- package/dist/config/generate.d.ts +114 -1
- package/dist/config/generate.js +106 -3
- package/dist/control/client.d.ts +5 -0
- package/dist/control/client.js +8 -0
- package/dist/control/daemon.d.ts +116 -0
- package/dist/control/daemon.js +339 -0
- package/dist/control/discovery.d.ts +26 -0
- package/dist/control/discovery.js +41 -9
- package/dist/control/ensure-hook.d.ts +39 -0
- package/dist/control/ensure-hook.js +98 -0
- package/dist/control/paths.d.ts +14 -0
- package/dist/control/paths.js +20 -0
- package/dist/control/server.d.ts +28 -0
- package/dist/control/server.js +15 -2
- package/dist/proxy/session.d.ts +8 -1
- package/dist/proxy/session.js +28 -6
- package/docs/calculatedReplay.md +51 -0
- package/docs/calculatedReplayGuide.md +471 -74
- package/docs/installRun.md +457 -111
- package/docs/loginWeb.md +1 -1
- package/docs/quickstart.md +195 -158
- package/package.json +2 -1
- package/scripts/install.ps1 +669 -0
- package/scripts/install.sh +586 -0
package/dist/control/server.d.ts
CHANGED
|
@@ -53,6 +53,14 @@ export interface ControlServerOptions {
|
|
|
53
53
|
cwd: string;
|
|
54
54
|
/** Preferred port; 0 (or a busy port) falls back to an ephemeral one. */
|
|
55
55
|
port?: number;
|
|
56
|
+
/**
|
|
57
|
+
* Whether a busy `port` may fall back to an ephemeral one. Default true — a
|
|
58
|
+
* recorder in a terminal is better on some port than not at all. A background
|
|
59
|
+
* recorder whose port is written into a project's hooks passes false: on a
|
|
60
|
+
* fallback port it would answer SessionStart and then every other hook would
|
|
61
|
+
* post into the void, which is worse than exiting with a reason.
|
|
62
|
+
*/
|
|
63
|
+
portFallback?: boolean;
|
|
56
64
|
/** Loopback bearer token. Generated when omitted. */
|
|
57
65
|
token?: string;
|
|
58
66
|
/** Config keys of the servers a proxy wraps. Grows as proxies register. */
|
|
@@ -90,6 +98,26 @@ export interface ControlServerOptions {
|
|
|
90
98
|
* recording and nothing else happens.
|
|
91
99
|
*/
|
|
92
100
|
replay?: ReplayOptions;
|
|
101
|
+
/**
|
|
102
|
+
* Where the replay switches came from — the environment, the project's
|
|
103
|
+
* stored policy, or the defaults. Reported on `/health` so `bir doctor` can
|
|
104
|
+
* say which, because "the allow-list I set is gone" has exactly one cause.
|
|
105
|
+
*/
|
|
106
|
+
replaySource?: "env" | "sidecar" | "default";
|
|
107
|
+
/**
|
|
108
|
+
* Whose account the recordings land in, for `/health` and `bir doctor`. "The
|
|
109
|
+
* run is not in Recordings" is, more often than not, "it is in somebody
|
|
110
|
+
* else's", and nothing said which until now.
|
|
111
|
+
*/
|
|
112
|
+
account?: string;
|
|
113
|
+
/**
|
|
114
|
+
* What 'POST /control/stop' does after answering. 'bir-hooks' passes its own
|
|
115
|
+
* shutdown; a test passes nothing and the route merely says it would. The
|
|
116
|
+
* route exists so a background recorder can be stopped the way Ctrl-C stops
|
|
117
|
+
* one in a terminal — finishing the run and draining the queue — rather than
|
|
118
|
+
* killed, which on Windows is the only other option.
|
|
119
|
+
*/
|
|
120
|
+
onStopRequested?: () => void;
|
|
93
121
|
}
|
|
94
122
|
export interface ControlServerAddress {
|
|
95
123
|
url: string;
|
package/dist/control/server.js
CHANGED
|
@@ -38,6 +38,7 @@ import { calculateCostUsd } from "../replay/pricing.js";
|
|
|
38
38
|
import { logDetail, logLine, errText } from "../util/log.js";
|
|
39
39
|
import { journal } from "../util/journal.js";
|
|
40
40
|
import { packageVersion } from "../util/version.js";
|
|
41
|
+
import { resolveAuthUrl } from "../auth/client.js";
|
|
41
42
|
/** How long a `/tool/post` waits for the proxy's own report before recording its own view. */
|
|
42
43
|
const PROXY_REPORT_GRACE_MS = 1_500;
|
|
43
44
|
/** One MCP call, as seen from up to two sides. */
|
|
@@ -127,7 +128,7 @@ export class ControlServer {
|
|
|
127
128
|
resolve(this.address);
|
|
128
129
|
});
|
|
129
130
|
};
|
|
130
|
-
bind(preferred, preferred !== 0);
|
|
131
|
+
bind(preferred, preferred !== 0 && this.opts.portFallback !== false);
|
|
131
132
|
});
|
|
132
133
|
}
|
|
133
134
|
async close() {
|
|
@@ -150,6 +151,10 @@ export class ControlServer {
|
|
|
150
151
|
const sessions = [...this.sessions.values()].map((s) => ({
|
|
151
152
|
sessionId: s.sessionId,
|
|
152
153
|
runId: s.run?.runId,
|
|
154
|
+
// Mid-run right now. What 'ensureDaemon' reads before it dares to restart
|
|
155
|
+
// an out-of-date recorder: a restart between turns costs nothing, one
|
|
156
|
+
// during a turn loses the turn.
|
|
157
|
+
active: Boolean(s.run) && !s.run.finished,
|
|
153
158
|
steps: s.run?.ordering.next ?? 0,
|
|
154
159
|
recording: s.run?.recording ?? false,
|
|
155
160
|
replay: s.run?.replay
|
|
@@ -187,12 +192,13 @@ export class ControlServer {
|
|
|
187
192
|
tier: "bound",
|
|
188
193
|
// The authoritative answer to "is anything actually being saved?".
|
|
189
194
|
recording: this.opts.recording ?? !(this.recorder instanceof NullRecorder),
|
|
195
|
+
account: this.opts.account ?? null,
|
|
190
196
|
// Whether a handed-out segment may actually run mid-task, or whether the
|
|
191
197
|
// runner is only watching (R-OUT-10, R-LIFE-8). Observe-only must never
|
|
192
198
|
// be invisible: an operator has to be able to see which of the two this
|
|
193
199
|
// machine is doing without reading a log file.
|
|
194
200
|
segmentArm: this.replay.segmentArm,
|
|
195
|
-
authUrl:
|
|
201
|
+
authUrl: resolveAuthUrl() || null,
|
|
196
202
|
sessionId: this.sessionId,
|
|
197
203
|
pid: process.pid,
|
|
198
204
|
cwd: this.opts.cwd,
|
|
@@ -210,6 +216,7 @@ export class ControlServer {
|
|
|
210
216
|
// (docs/calculatedReplay.md §13.2, mitigation 3).
|
|
211
217
|
replay: {
|
|
212
218
|
enabled: this.replay.enabled,
|
|
219
|
+
source: this.opts.replaySource ?? null,
|
|
213
220
|
minSimilarity: this.opts.replay?.minSimilarity ?? null,
|
|
214
221
|
allowServers: this.opts.replay?.allowServers ? [...this.opts.replay.allowServers] : null,
|
|
215
222
|
deriveKey: Boolean(this.opts.replay?.apiKey ?? process.env.ANTHROPIC_API_KEY),
|
|
@@ -248,6 +255,12 @@ export class ControlServer {
|
|
|
248
255
|
}
|
|
249
256
|
const body = await this.readJson(req);
|
|
250
257
|
switch (route) {
|
|
258
|
+
case "/control/stop":
|
|
259
|
+
// Answer first, then leave: the caller is polling for the pid to go.
|
|
260
|
+
this.send(res, 200, { ok: Boolean(this.opts.onStopRequested) });
|
|
261
|
+
if (this.opts.onStopRequested)
|
|
262
|
+
setImmediate(() => this.opts.onStopRequested?.());
|
|
263
|
+
return;
|
|
251
264
|
case "/session/start":
|
|
252
265
|
this.send(res, 200, this.onSessionStart(body));
|
|
253
266
|
return;
|
package/dist/proxy/session.d.ts
CHANGED
|
@@ -25,7 +25,14 @@ import type { UpstreamClient } from "../upstream/client.js";
|
|
|
25
25
|
import type { ProxyStepReport } from "../control/correlation.js";
|
|
26
26
|
import { type Recorder } from "../record/recorder.js";
|
|
27
27
|
/** How long a proxy waits for a control server before falling to Tier 2. */
|
|
28
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Ten seconds, not five: the recorder is now started by the SessionStart hook,
|
|
30
|
+
* which fires while the host is also spawning this proxy, and a cold start
|
|
31
|
+
* (sign-in refresh, the service's /health probe, then listen) has been
|
|
32
|
+
* measured at one to four seconds. Steps are buffered meanwhile, never
|
|
33
|
+
* dropped, and a host session is never waiting on this.
|
|
34
|
+
*/
|
|
35
|
+
export declare const DISCOVERY_WINDOW_MS = 10000;
|
|
29
36
|
export type Tier = "bound" | "standalone" | "pending";
|
|
30
37
|
export interface ProxySessionOptions {
|
|
31
38
|
serverName: string;
|
package/dist/proxy/session.js
CHANGED
|
@@ -26,7 +26,7 @@ import { hostname } from "node:os";
|
|
|
26
26
|
import { ControlClient } from "../control/client.js";
|
|
27
27
|
import { resolveControl } from "../control/discovery.js";
|
|
28
28
|
import { qualifyToolName } from "../control/correlation.js";
|
|
29
|
-
import { authenticate } from "../auth/client.js";
|
|
29
|
+
import { authenticate, resolveAuthUrl } from "../auth/client.js";
|
|
30
30
|
import { NullRecorder } from "../record/recorder.js";
|
|
31
31
|
import { RemoteRecorder } from "../record/remote-recorder.js";
|
|
32
32
|
import { StepQueue } from "../record/queue.js";
|
|
@@ -36,7 +36,14 @@ import { redact } from "../record/redact.js";
|
|
|
36
36
|
import { logLine, logDetail, errText } from "../util/log.js";
|
|
37
37
|
import { packageVersion } from "../util/version.js";
|
|
38
38
|
/** How long a proxy waits for a control server before falling to Tier 2. */
|
|
39
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Ten seconds, not five: the recorder is now started by the SessionStart hook,
|
|
41
|
+
* which fires while the host is also spawning this proxy, and a cold start
|
|
42
|
+
* (sign-in refresh, the service's /health probe, then listen) has been
|
|
43
|
+
* measured at one to four seconds. Steps are buffered meanwhile, never
|
|
44
|
+
* dropped, and a host session is never waiting on this.
|
|
45
|
+
*/
|
|
46
|
+
export const DISCOVERY_WINDOW_MS = 10_000;
|
|
40
47
|
export class ProxySession {
|
|
41
48
|
serverName;
|
|
42
49
|
/** This proxy process, as the control server tells proxies apart (replay/executor.ts). */
|
|
@@ -98,6 +105,7 @@ export class ProxySession {
|
|
|
98
105
|
return;
|
|
99
106
|
}
|
|
100
107
|
const window = this.opts.discoveryWindowMs ?? DISCOVERY_WINDOW_MS;
|
|
108
|
+
const deadline = Date.now() + window;
|
|
101
109
|
const found = await resolveControl(this.opts.cwd, window);
|
|
102
110
|
if (!found?.url) {
|
|
103
111
|
await this.becomeStandalone(`no control server within ${window}ms`);
|
|
@@ -108,12 +116,24 @@ export class ProxySession {
|
|
|
108
116
|
startedAt: this.startedAt,
|
|
109
117
|
pid: process.pid,
|
|
110
118
|
});
|
|
111
|
-
const
|
|
119
|
+
const info = {
|
|
112
120
|
serverName: this.serverName,
|
|
113
121
|
pid: process.pid,
|
|
114
122
|
cwd: this.opts.cwd,
|
|
115
123
|
version: packageVersion(),
|
|
116
|
-
}
|
|
124
|
+
};
|
|
125
|
+
// A control server that is still coming up — the SessionStart hook is
|
|
126
|
+
// starting it while the host spawns us — answers nothing for a moment.
|
|
127
|
+
// Keep trying inside the same window rather than settle for Tier 2 on the
|
|
128
|
+
// first refusal.
|
|
129
|
+
let registered = await client.register(info);
|
|
130
|
+
while (!registered && Date.now() < deadline) {
|
|
131
|
+
await new Promise((resolve) => {
|
|
132
|
+
const t = setTimeout(resolve, 250);
|
|
133
|
+
t.unref?.();
|
|
134
|
+
});
|
|
135
|
+
registered = await client.register(info);
|
|
136
|
+
}
|
|
117
137
|
if (!registered) {
|
|
118
138
|
await this.becomeStandalone("control server did not answer /proxy/register");
|
|
119
139
|
return;
|
|
@@ -216,9 +236,11 @@ export class ProxySession {
|
|
|
216
236
|
if (this.opts.recorderFactory) {
|
|
217
237
|
return (await this.opts.recorderFactory()) ?? new NullRecorder();
|
|
218
238
|
}
|
|
219
|
-
|
|
239
|
+
// BIR_AUTH_URL, or the address 'bir setup' stored — the same lookup the
|
|
240
|
+
// control server and the CLI make, so no two processes disagree about it.
|
|
241
|
+
const baseUrl = resolveAuthUrl();
|
|
220
242
|
if (!baseUrl) {
|
|
221
|
-
logLine("recorder.disabled", { why: "
|
|
243
|
+
logLine("recorder.disabled", { why: "no service address — run 'bir setup', or set BIR_AUTH_URL" });
|
|
222
244
|
return new NullRecorder();
|
|
223
245
|
}
|
|
224
246
|
// A proxy runs inside the host's process tree with its stdio bound to the
|
package/docs/calculatedReplay.md
CHANGED
|
@@ -814,6 +814,23 @@ 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`, 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 now sandboxes its own evaluation of every logic body: one worker thread with an empty
|
|
827
|
+
environment and a memory cap, a fresh `node:vm` context per evaluation with a time limit and nothing
|
|
828
|
+
but JavaScript's own built-ins, values in and out as JSON text only. It had to: there, one account's
|
|
829
|
+
code runs in the process that holds every account's data and the service's secrets. **The runner
|
|
830
|
+
still does not sandbox.** Here the code runs on the owner's machine, for the owner's account. One
|
|
831
|
+
consequence to know: code that uses `fetch`, `process` or a timer throws in the service's check, so
|
|
832
|
+
it cannot be saved without `--force`; a forced step would run here with all of them.
|
|
833
|
+
|
|
817
834
|
### 13.2 Replay bypasses permission prompts
|
|
818
835
|
|
|
819
836
|
This is the sharpest edge in the whole design, and it must not be buried.
|
|
@@ -1038,12 +1055,46 @@ bir uninstall --replay reverse exactly that, leaving the rest of the inst
|
|
|
1038
1055
|
|
|
1039
1056
|
bir scenario list GET /recordings/runs runs, iterations, scenario state
|
|
1040
1057
|
bir scenario show <runId> GET /recordings/runs/:id/scenario intent, params, steps, logic
|
|
1058
|
+
bir scenario show <scnId> GET /scenarios/:id the same, by scenario id (segments too)
|
|
1059
|
+
[--step <n>: one step]
|
|
1041
1060
|
bir scenario calc <runId> POST /recordings/runs/:id/calculate [--force to re-derive in place]
|
|
1061
|
+
[--force --discard-edits: re-derive a plan with hand edits]
|
|
1042
1062
|
bir scenario replay <scnId> --prompt "…" [--dry]
|
|
1043
1063
|
|
|
1064
|
+
bir scenario check <id> --step <n> POST /scenarios/:id/steps/:n/check try a change; writes nothing
|
|
1065
|
+
bir scenario edit <id> --step <n> PATCH /scenarios/:id/steps/:n the same check, then save
|
|
1066
|
+
bir scenario edits <id> GET /scenarios/:id/edits the history, newest first
|
|
1067
|
+
bir scenario undo <id> [--edit <e>] POST /scenarios/:id/edits/:e/revert put one step back
|
|
1068
|
+
bir scenario editing on|off|status (no route) ~/.baseinstrunner/installed.json which `bir` MCP tools are offered
|
|
1069
|
+
|
|
1044
1070
|
bir replay --scenario <scnId> --prompt "…" [--dry] alias; the Tier 2 / debugging path (§12.1)
|
|
1045
1071
|
```
|
|
1046
1072
|
|
|
1073
|
+
The second group, and `show <scnId>`, `--step` and `--discard-edits`, were added on 2026-09-25 by
|
|
1074
|
+
the service's `editSteps.md` and are not yet released; their routes are new on the service. They
|
|
1075
|
+
are the one place `bir` changes a scenario, so the rules are strict:
|
|
1076
|
+
|
|
1077
|
+
- **Owner only.** Anyone else, an admin included, gets `404`, never `403`.
|
|
1078
|
+
- **A change is saved only if it reproduces the recording and is not a copy**, or on purpose with
|
|
1079
|
+
`--force --note "<why>"`. `check` and `edit` run the same check; `check` never writes.
|
|
1080
|
+
- `<id>` is a `run_` id, resolved to its whole-run scenario with `GET /recordings/runs/:id/scenario`,
|
|
1081
|
+
or a `scn_` id, used as it is. Logic goes in with `--input-logic` / `--output-logic` from a file,
|
|
1082
|
+
or `-` for stdin — never inline, because shell quoting mangles JavaScript. `edit` also takes
|
|
1083
|
+
`--freeze`, `--unfreeze`, `--note` and `--revision <n>` (the `expectedRevision` of the body).
|
|
1084
|
+
- `undo` without `--edit` undoes the newest edit that can be undone. Every save and every undo bumps
|
|
1085
|
+
the scenario's `chainRevision`.
|
|
1086
|
+
- `calc --force` answers `409 scenario_has_edits` on a plan with hand edits, unless
|
|
1087
|
+
`--discard-edits` is given.
|
|
1088
|
+
- Exit codes: `0` checked OK, saved or undone; `1` refused, not found, or a service error; `2`
|
|
1089
|
+
usage. `--json` prints the service's body unchanged.
|
|
1090
|
+
|
|
1091
|
+
The same commands are tools on the `bir` MCP server. `scenario_show`, `scenario_edits` and
|
|
1092
|
+
`investigate` only read, and are always offered. `scenario_check`, `scenario_edit` and
|
|
1093
|
+
`scenario_undo` are offered only in a project where `bir scenario editing on` was run, because that
|
|
1094
|
+
server runs in every session of every installed project. Each tool runs the installed `bir … --json`;
|
|
1095
|
+
a refusal comes back as an MCP error result carrying the report. The operator's walk-through is
|
|
1096
|
+
[calculatedReplayGuide.md](calculatedReplayGuide.md) §9.2.
|
|
1097
|
+
|
|
1047
1098
|
`--dry` maps to BaseIn's `POST /scenarios/:id/replay`, which evaluates the stored logic against the
|
|
1048
1099
|
source run's *recorded* outputs — no real tools, no side effects, one Haiku call. Without `--dry`,
|
|
1049
1100
|
`bir replay` runs the same plan through the same executor against the live proxies. `bir doctor`
|