opensip-cli 0.1.8 → 0.1.10
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/dist/bootstrap/admit-tool-package.d.ts +51 -11
- package/dist/bootstrap/admit-tool-package.d.ts.map +1 -1
- package/dist/bootstrap/admit-tool-package.js +46 -12
- package/dist/bootstrap/admit-tool-package.js.map +1 -1
- package/dist/bootstrap/baseline-seams.js +1 -1
- package/dist/bootstrap/baseline-seams.js.map +1 -1
- package/dist/bootstrap/bind-external-dispatch.d.ts +36 -0
- package/dist/bootstrap/bind-external-dispatch.d.ts.map +1 -0
- package/dist/bootstrap/bind-external-dispatch.js +81 -0
- package/dist/bootstrap/bind-external-dispatch.js.map +1 -0
- package/dist/bootstrap/build-command-registration-input.d.ts +13 -2
- package/dist/bootstrap/build-command-registration-input.d.ts.map +1 -1
- package/dist/bootstrap/build-command-registration-input.js +29 -2
- package/dist/bootstrap/build-command-registration-input.js.map +1 -1
- package/dist/bootstrap/build-per-run-scope.d.ts.map +1 -1
- package/dist/bootstrap/build-per-run-scope.js +19 -2
- package/dist/bootstrap/build-per-run-scope.js.map +1 -1
- package/dist/bootstrap/config-and-capabilities.d.ts +21 -6
- package/dist/bootstrap/config-and-capabilities.d.ts.map +1 -1
- package/dist/bootstrap/config-and-capabilities.js +79 -23
- package/dist/bootstrap/config-and-capabilities.js.map +1 -1
- package/dist/bootstrap/dispatch-external-tool-command.d.ts +67 -0
- package/dist/bootstrap/dispatch-external-tool-command.d.ts.map +1 -0
- package/dist/bootstrap/dispatch-external-tool-command.js +79 -0
- package/dist/bootstrap/dispatch-external-tool-command.js.map +1 -0
- package/dist/bootstrap/dispatch-external-tool-hook.d.ts +47 -0
- package/dist/bootstrap/dispatch-external-tool-hook.d.ts.map +1 -0
- package/dist/bootstrap/dispatch-external-tool-hook.js +49 -0
- package/dist/bootstrap/dispatch-external-tool-hook.js.map +1 -0
- package/dist/bootstrap/dispatch-fork-core.d.ts +48 -0
- package/dist/bootstrap/dispatch-fork-core.d.ts.map +1 -0
- package/dist/bootstrap/dispatch-fork-core.js +208 -0
- package/dist/bootstrap/dispatch-fork-core.js.map +1 -0
- package/dist/bootstrap/dispatch-host-rpc-handler.d.ts +27 -0
- package/dist/bootstrap/dispatch-host-rpc-handler.d.ts.map +1 -0
- package/dist/bootstrap/dispatch-host-rpc-handler.js +175 -0
- package/dist/bootstrap/dispatch-host-rpc-handler.js.map +1 -0
- package/dist/bootstrap/dispatch-replay-result.d.ts +51 -0
- package/dist/bootstrap/dispatch-replay-result.d.ts.map +1 -0
- package/dist/bootstrap/dispatch-replay-result.js +76 -0
- package/dist/bootstrap/dispatch-replay-result.js.map +1 -0
- package/dist/bootstrap/execute-post-bailout-bootstrap.d.ts.map +1 -1
- package/dist/bootstrap/execute-post-bailout-bootstrap.js +3 -1
- package/dist/bootstrap/execute-post-bailout-bootstrap.js.map +1 -1
- package/dist/bootstrap/owning-tool-init.d.ts +8 -2
- package/dist/bootstrap/owning-tool-init.d.ts.map +1 -1
- package/dist/bootstrap/owning-tool-init.js +11 -1
- package/dist/bootstrap/owning-tool-init.js.map +1 -1
- package/dist/bootstrap/register-authored-tools.d.ts +49 -0
- package/dist/bootstrap/register-authored-tools.d.ts.map +1 -0
- package/dist/bootstrap/register-authored-tools.js +132 -0
- package/dist/bootstrap/register-authored-tools.js.map +1 -0
- package/dist/bootstrap/register-tools-discovery.d.ts +0 -32
- package/dist/bootstrap/register-tools-discovery.d.ts.map +1 -1
- package/dist/bootstrap/register-tools-discovery.js +36 -100
- package/dist/bootstrap/register-tools-discovery.js.map +1 -1
- package/dist/bootstrap/register-tools-mount.d.ts.map +1 -1
- package/dist/bootstrap/register-tools-mount.js +20 -44
- package/dist/bootstrap/register-tools-mount.js.map +1 -1
- package/dist/bootstrap/register-tools.d.ts +2 -1
- package/dist/bootstrap/register-tools.d.ts.map +1 -1
- package/dist/bootstrap/register-tools.js +2 -1
- package/dist/bootstrap/register-tools.js.map +1 -1
- package/dist/bootstrap/run-plane.d.ts +11 -0
- package/dist/bootstrap/run-plane.d.ts.map +1 -1
- package/dist/bootstrap/run-plane.js.map +1 -1
- package/dist/bootstrap/synthesize-external-tool.d.ts +45 -0
- package/dist/bootstrap/synthesize-external-tool.d.ts.map +1 -0
- package/dist/bootstrap/synthesize-external-tool.js +112 -0
- package/dist/bootstrap/synthesize-external-tool.js.map +1 -0
- package/dist/bootstrap/tool-command-dispatch-types.d.ts +280 -0
- package/dist/bootstrap/tool-command-dispatch-types.d.ts.map +1 -0
- package/dist/bootstrap/tool-command-dispatch-types.js +34 -0
- package/dist/bootstrap/tool-command-dispatch-types.js.map +1 -0
- package/dist/bootstrap/tool-command-worker-config-pass.d.ts +24 -0
- package/dist/bootstrap/tool-command-worker-config-pass.d.ts.map +1 -0
- package/dist/bootstrap/tool-command-worker-config-pass.js +52 -0
- package/dist/bootstrap/tool-command-worker-config-pass.js.map +1 -0
- package/dist/bootstrap/tool-command-worker-context.d.ts +55 -0
- package/dist/bootstrap/tool-command-worker-context.d.ts.map +1 -0
- package/dist/bootstrap/tool-command-worker-context.js +163 -0
- package/dist/bootstrap/tool-command-worker-context.js.map +1 -0
- package/dist/bootstrap/tool-command-worker-entry.d.ts +66 -0
- package/dist/bootstrap/tool-command-worker-entry.d.ts.map +1 -0
- package/dist/bootstrap/tool-command-worker-entry.js +298 -0
- package/dist/bootstrap/tool-command-worker-entry.js.map +1 -0
- package/dist/bootstrap/tool-command-worker-rpc.d.ts +53 -0
- package/dist/bootstrap/tool-command-worker-rpc.d.ts.map +1 -0
- package/dist/bootstrap/tool-command-worker-rpc.js +78 -0
- package/dist/bootstrap/tool-command-worker-rpc.js.map +1 -0
- package/dist/bootstrap/tool-provenance.d.ts +85 -0
- package/dist/bootstrap/tool-provenance.d.ts.map +1 -0
- package/dist/bootstrap/tool-provenance.js +101 -0
- package/dist/bootstrap/tool-provenance.js.map +1 -0
- package/dist/cli-context.d.ts +17 -0
- package/dist/cli-context.d.ts.map +1 -1
- package/dist/cli-context.js +62 -1
- package/dist/cli-context.js.map +1 -1
- package/dist/commands/completion.d.ts.map +1 -1
- package/dist/commands/completion.js +3 -0
- package/dist/commands/completion.js.map +1 -1
- package/dist/commands/host-command-specs.d.ts +13 -15
- package/dist/commands/host-command-specs.d.ts.map +1 -1
- package/dist/commands/host-command-specs.js +27 -27
- package/dist/commands/host-command-specs.js.map +1 -1
- package/dist/commands/host-subcommand-groups.d.ts.map +1 -1
- package/dist/commands/host-subcommand-groups.js +63 -5
- package/dist/commands/host-subcommand-groups.js.map +1 -1
- package/dist/commands/internal-command-visibility.d.ts +13 -4
- package/dist/commands/internal-command-visibility.d.ts.map +1 -1
- package/dist/commands/internal-command-visibility.js +14 -5
- package/dist/commands/internal-command-visibility.js.map +1 -1
- package/dist/commands/mount-command-spec.d.ts.map +1 -1
- package/dist/commands/mount-command-spec.js +31 -0
- package/dist/commands/mount-command-spec.js.map +1 -1
- package/dist/commands/session-show.d.ts.map +1 -1
- package/dist/commands/session-show.js +4 -1
- package/dist/commands/session-show.js.map +1 -1
- package/dist/commands/tools/data-purge.js +2 -2
- package/dist/commands/tools/data-purge.js.map +1 -1
- package/dist/commands/tools/validate.js +1 -1
- package/dist/env/host-env-specs.d.ts.map +1 -1
- package/dist/env/host-env-specs.js +6 -0
- package/dist/env/host-env-specs.js.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/report-compose.d.ts.map +1 -1
- package/dist/report-compose.js +85 -19
- package/dist/report-compose.js.map +1 -1
- package/dist/session-replay-registry.d.ts +33 -6
- package/dist/session-replay-registry.d.ts.map +1 -1
- package/dist/session-replay-registry.js +43 -6
- package/dist/session-replay-registry.js.map +1 -1
- package/dist/telemetry/profiling.d.ts +30 -0
- package/dist/telemetry/profiling.d.ts.map +1 -1
- package/dist/telemetry/profiling.js +16 -1
- package/dist/telemetry/profiling.js.map +1 -1
- package/package.json +32 -32
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tool-command-worker-entry — the WORKER side of the out-of-process external
|
|
3
|
+
* tool command dispatch plane (ADR-0054, increments M4-C / M4-D / M4-E).
|
|
4
|
+
*
|
|
5
|
+
* This is a HOST internal `CommandSpec` (`__tool-command-worker`), forked by the
|
|
6
|
+
* supervisor as `node <cliScript> __tool-command-worker <specPath> --cwd <cwd>`.
|
|
7
|
+
* Forking the CLI binary as a subcommand (the SAME pattern graph's
|
|
8
|
+
* `graph-run-worker` uses) means the FULL CLI bootstrap runs first: the preAction
|
|
9
|
+
* hook discovers + imports the external tool runtime IN THE WORKER, registers it,
|
|
10
|
+
* runs its `contributeScope`, composes + validates config, and builds the full
|
|
11
|
+
* per-run scope — so by the time this handler runs, `currentScope()` carries the
|
|
12
|
+
* tool's subscope (`scope.fitness`/…), the check/recipe registries, project
|
|
13
|
+
* context, and `toolConfig` exactly as an in-process run (ADR-0054 M4-C `scope`
|
|
14
|
+
* mapping: "the worker re-bootstraps its OWN scope … exactly like graph's
|
|
15
|
+
* worker"). This is the isolation move — the untrusted runtime loads HERE, in the
|
|
16
|
+
* worker, never in the host.
|
|
17
|
+
*
|
|
18
|
+
* The handler then resolves the dispatched tool from the re-bootstrapped registry
|
|
19
|
+
* and runs ITS command handler against the WORKER-side `ToolCliContext` shim
|
|
20
|
+
* (`tool-command-worker-context.ts`): FRR seams (render/json/envelope/raw/error/
|
|
21
|
+
* exit) record the value and return it once in the {@link ToolCommandResult}; the
|
|
22
|
+
* host-RPC seams (datastore / egress / SARIF / baselines / toolState / hostPlanes
|
|
23
|
+
* / report-open / exit-code re-affirm) UPCALL the host over the rpc-reply channel
|
|
24
|
+
* (the host performs the privileged effect — datastore/network/FS/exit stay
|
|
25
|
+
* host-owned). Only the live-view seams fail loud (`unsupported-seam`).
|
|
26
|
+
*
|
|
27
|
+
* A handler that calls `process.exit`, throws, crashes the native layer, or spins
|
|
28
|
+
* the event loop is contained: the supervisor turns a premature child exit /
|
|
29
|
+
* timeout / `error` message into a structured parent-side failure, and the host
|
|
30
|
+
* process survives.
|
|
31
|
+
*/
|
|
32
|
+
import { readFileSync } from 'node:fs';
|
|
33
|
+
import { createRunTimer, currentScope, defineCommand, resolveToolHooks, } from '@opensip-cli/core';
|
|
34
|
+
import { runDeepConfigPass } from './tool-command-worker-config-pass.js';
|
|
35
|
+
import { buildWorkerContext, UnsupportedSeamError, } from './tool-command-worker-context.js';
|
|
36
|
+
import { createWorkerRpcClient } from './tool-command-worker-rpc.js';
|
|
37
|
+
/** Post one IPC message to the parent (no-op when not forked — e.g. a unit call). */
|
|
38
|
+
function send(msg) {
|
|
39
|
+
process.send?.(msg);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Resolve the dispatched tool from the re-bootstrapped registry. The bootstrap
|
|
43
|
+
* already imported + registered it (the isolation import happened in this worker
|
|
44
|
+
* during preAction). Match by the registry's human key first, then by stable id /
|
|
45
|
+
* human name — symmetric to the host provenance/dispatch matchers.
|
|
46
|
+
*
|
|
47
|
+
* @throws {Error & {failureClass}} `runtime-load-failed` when the tool is not in
|
|
48
|
+
* the worker's registry (the bootstrap did not admit it — e.g. a trust-policy
|
|
49
|
+
* or discovery miss). Surfaces as a structured IPC error; the host survives.
|
|
50
|
+
*/
|
|
51
|
+
function resolveTool(spec) {
|
|
52
|
+
const tools = currentScope()?.tools;
|
|
53
|
+
const tool = tools?.get(spec.toolId) ??
|
|
54
|
+
tools?.list().find((t) => t.metadata.id === spec.toolId || t.metadata.name === spec.toolId);
|
|
55
|
+
if (tool === undefined) {
|
|
56
|
+
const err = new Error(`tool command worker: tool '${spec.toolId}' is not registered in the worker scope ` +
|
|
57
|
+
'(the bootstrap did not discover/admit it — check provenance/trust policy)');
|
|
58
|
+
err.failureClass = 'runtime-load-failed';
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
61
|
+
return tool;
|
|
62
|
+
}
|
|
63
|
+
/** Resolve the command spec the worker should run, or throw `command-not-found`. */
|
|
64
|
+
function findCommandSpec(tool, commandName) {
|
|
65
|
+
const spec = tool.commandSpecs?.find((s) => s.name === commandName);
|
|
66
|
+
if (spec === undefined) {
|
|
67
|
+
const err = new Error(`tool command worker: tool '${tool.metadata.id}' has no command '${commandName}'`);
|
|
68
|
+
err.failureClass = 'command-not-found';
|
|
69
|
+
throw err;
|
|
70
|
+
}
|
|
71
|
+
return spec;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* ADR-0054 M4-F: run the dispatched tool's `initialize()` once, worker-side,
|
|
75
|
+
* before its handler. The host no longer runs an EXTERNAL owning tool's
|
|
76
|
+
* `initialize` (that would execute untrusted runtime in the kernel) — and the
|
|
77
|
+
* worker bootstraps the host `__tool-command-worker` subcommand (owned by NO
|
|
78
|
+
* tool), so the worker's own preflight never resolves the dispatched tool as the
|
|
79
|
+
* "owning tool". Running it here is the only place an external tool's
|
|
80
|
+
* `initialize` runs under worker dispatch. A throw propagates to
|
|
81
|
+
* {@link runToolCommandWorker}'s catch → structured `tool-handler-throw`; the
|
|
82
|
+
* host survives (fail loud, never a half-initialised silent run).
|
|
83
|
+
*/
|
|
84
|
+
async function runWorkerInitialize(tool) {
|
|
85
|
+
const initialize = resolveToolHooks(tool).initialize;
|
|
86
|
+
if (initialize === undefined)
|
|
87
|
+
return;
|
|
88
|
+
await initialize();
|
|
89
|
+
}
|
|
90
|
+
/** Build a structured `error` IPC message with a failure class (+ stack when present). */
|
|
91
|
+
function errorMessage(message, failureClass, stack) {
|
|
92
|
+
return {
|
|
93
|
+
kind: 'error',
|
|
94
|
+
message,
|
|
95
|
+
failureClass,
|
|
96
|
+
...(stack === undefined ? {} : { stack }),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/** Read + parse the worker spec file, or return a `bad-spec` error message. */
|
|
100
|
+
function readSpec(specPath) {
|
|
101
|
+
try {
|
|
102
|
+
return JSON.parse(readFileSync(specPath, 'utf8'));
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
return errorMessage(`tool command worker: unreadable spec at '${specPath}': ${error instanceof Error ? error.message : String(error)}`, 'bad-spec');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The output modes whose PAYLOAD is the handler's RETURN value (routed by the
|
|
110
|
+
* in-process `dispatchOutput`): `command-result` (a `CommandResult`) and
|
|
111
|
+
* `signal-envelope` (a `SignalEnvelope`). For these, the worker must carry the
|
|
112
|
+
* return back UNROUTED in `returned` so the supervisor replays it through the SAME
|
|
113
|
+
* `dispatchOutput`. `raw-stream` / `live-view` produce no routable return payload.
|
|
114
|
+
*/
|
|
115
|
+
function isReturnValuedOutput(output) {
|
|
116
|
+
return output === 'command-result' || output === 'signal-envelope';
|
|
117
|
+
}
|
|
118
|
+
/** Drain the accumulator + the handler's return into a serializable result. */
|
|
119
|
+
function toResult(output, acc, session, returned) {
|
|
120
|
+
return {
|
|
121
|
+
output,
|
|
122
|
+
...(acc.render === undefined ? {} : { render: acc.render }),
|
|
123
|
+
...(acc.envelope === undefined ? {} : { envelope: acc.envelope }),
|
|
124
|
+
...(acc.json === undefined ? {} : { json: acc.json }),
|
|
125
|
+
...(acc.raw === undefined ? {} : { raw: acc.raw }),
|
|
126
|
+
...(acc.error === undefined ? {} : { error: acc.error }),
|
|
127
|
+
...(acc.exitCode === undefined ? {} : { exitCode: acc.exitCode }),
|
|
128
|
+
...(session === undefined ? {} : { session }),
|
|
129
|
+
// Carry the handler's return for the return-valued modes so the supervisor
|
|
130
|
+
// routes it via the same `dispatchOutput` the in-process path uses (parity).
|
|
131
|
+
...(returned === undefined || !isReturnValuedOutput(output) ? {} : { returned }),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/** Map a thrown error to its structured failure class for the IPC `error` message. */
|
|
135
|
+
function classifyThrow(error) {
|
|
136
|
+
if (error instanceof UnsupportedSeamError)
|
|
137
|
+
return error.failureClass;
|
|
138
|
+
return error.failureClass ?? 'tool-handler-throw';
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Resolve the dispatched tool from the re-bootstrapped scope, run the deep config
|
|
142
|
+
* pass, then run its command handler against the worker-side context shim, and
|
|
143
|
+
* build the result. Throws on a handler error (caught by
|
|
144
|
+
* {@link runToolCommandWorker}); returns an `error` message for the structured
|
|
145
|
+
* pre-handler failures (config-invalid; tool / command-not-found are thrown with
|
|
146
|
+
* a failureClass tag).
|
|
147
|
+
*
|
|
148
|
+
* `currentScope()` here is the FULL per-run scope the CLI bootstrap built for the
|
|
149
|
+
* `__tool-command-worker` subcommand (project/config/registries/contributeScope),
|
|
150
|
+
* so the handler reads `cli.scope.toolConfig`/`cli.scope.<subscope>`/checks
|
|
151
|
+
* worker-LOCAL while datastore/egress cross to the host via the RPC shim.
|
|
152
|
+
*/
|
|
153
|
+
async function runLoadedCommand(spec) {
|
|
154
|
+
const tool = resolveTool(spec);
|
|
155
|
+
// ADR-0054 M4-F hook mode: when the spec names a lifecycle HOOK (not a command),
|
|
156
|
+
// run that hook worker-side and return its plain-data result. This is how the
|
|
157
|
+
// host gathers an external tool's `collectReportData` / `sessionReplay` without
|
|
158
|
+
// executing the untrusted runtime in the kernel process.
|
|
159
|
+
if (spec.hook !== undefined) {
|
|
160
|
+
return await runLoadedHook(tool, spec);
|
|
161
|
+
}
|
|
162
|
+
if (spec.commandName === undefined) {
|
|
163
|
+
return errorMessage(`tool command worker: spec for tool '${spec.toolId}' names neither a command nor a hook`, 'bad-spec');
|
|
164
|
+
}
|
|
165
|
+
const commandSpec = findCommandSpec(tool, spec.commandName);
|
|
166
|
+
// ADR-0054 M4-E DEEP config pass: run the tool's REAL Zod against its config
|
|
167
|
+
// namespace IN THE WORKER (the host validated only the coarse manifest shape
|
|
168
|
+
// pre-fork). A failure crosses IPC as `config-invalid` — never a host crash —
|
|
169
|
+
// and the supervisor maps it to the SAME typed config error the host coarse
|
|
170
|
+
// pass uses. Runs BEFORE building the context: a config failure must
|
|
171
|
+
// short-circuit before any handler effect.
|
|
172
|
+
const configFailure = runDeepConfigPass(tool, spec.config);
|
|
173
|
+
if (configFailure !== undefined)
|
|
174
|
+
return errorMessage(configFailure, 'config-invalid');
|
|
175
|
+
// ADR-0054 M4-F: run the dispatched tool's `initialize()` worker-side before the
|
|
176
|
+
// handler (the host no longer runs an external owning tool's initialize). A
|
|
177
|
+
// throw becomes a structured `tool-handler-throw` via the outer catch.
|
|
178
|
+
await runWorkerInitialize(tool);
|
|
179
|
+
// The host-RPC upcall client over the live IPC channel (M4-C). `process` is the
|
|
180
|
+
// duplex: requests post via `process.send`; replies arrive on
|
|
181
|
+
// `process.on('message')`. Disposed in the finally so the listener is removed.
|
|
182
|
+
const rpcClient = createWorkerRpcClient(process);
|
|
183
|
+
// The handler runs against the bootstrapped scope (worker-local reads) but with
|
|
184
|
+
// the WORKER context shim (FRR records + RPC upcalls for privileged effects).
|
|
185
|
+
const scope = currentScope();
|
|
186
|
+
if (scope === undefined) {
|
|
187
|
+
return errorMessage('tool command worker: no scope is entered (bootstrap did not run before the worker handler)', 'runtime-load-failed');
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
const acc = {};
|
|
191
|
+
const ctx = buildWorkerContext(scope, createRunTimer(), acc, rpcClient);
|
|
192
|
+
// Run the handler. A `process.exit` / crash / hang here is contained by the
|
|
193
|
+
// supervisor (premature-exit / timeout → structured parent failure); a throw
|
|
194
|
+
// propagates to runToolCommandWorker's catch and becomes a structured error.
|
|
195
|
+
// The handler's RETURN serves two roles: (1) for `command-result` /
|
|
196
|
+
// `signal-envelope` it IS the output payload (routed by `dispatchOutput`
|
|
197
|
+
// host-side); (2) it may carry a `session` leg (ToolRunCompletion) the host
|
|
198
|
+
// persists after the worker resolves (host-owned-run-timing). Capture it once.
|
|
199
|
+
const returned = (await commandSpec.handler({ ...spec.opts, _args: spec.positionals }, ctx));
|
|
200
|
+
return {
|
|
201
|
+
kind: 'result',
|
|
202
|
+
value: toResult(commandSpec.output, acc, returned?.session, returned),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
finally {
|
|
206
|
+
// @fitness-ignore-next-line detached-promises -- WorkerRpcClient.dispose() returns void (removes the reply listener + clears the pending map, synchronous); the name-based heuristic misfires inside this async fn.
|
|
207
|
+
rpcClient.dispose();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* ADR-0054 M4-F: run a dispatched tool's LIFECYCLE HOOK worker-side and return its
|
|
212
|
+
* plain-data result in `hookResult`. This is how the host gathers an EXTERNAL
|
|
213
|
+
* tool's `collectReportData` / `sessionReplay` data WITHOUT executing the
|
|
214
|
+
* untrusted runtime in the kernel process. The hook runs against the worker's own
|
|
215
|
+
* re-bootstrapped scope (`currentScope()`), exactly the contract the in-host path
|
|
216
|
+
* gives a bundled tool — just inside the isolation boundary. The host owns the
|
|
217
|
+
* merge/render/replay-emit (privileged effect). A throw propagates to
|
|
218
|
+
* {@link runToolCommandWorker}'s catch → structured failure; the host survives.
|
|
219
|
+
*
|
|
220
|
+
* The hook's runtime is resolved from the re-bootstrapped registry (the worker
|
|
221
|
+
* `initialize()` does NOT run for a hook-mode dispatch — hooks read data; an
|
|
222
|
+
* external tool that needs `initialize` for its report/replay should run it in
|
|
223
|
+
* the hook body, which is OUT of M4-F's first-party scope).
|
|
224
|
+
*/
|
|
225
|
+
async function runLoadedHook(tool, spec) {
|
|
226
|
+
const scope = currentScope();
|
|
227
|
+
if (scope === undefined) {
|
|
228
|
+
return errorMessage('tool command worker: no scope is entered for the hook run (bootstrap did not run)', 'runtime-load-failed');
|
|
229
|
+
}
|
|
230
|
+
const hooks = resolveToolHooks(tool);
|
|
231
|
+
// `collectReportData(scope)` takes the tool-facing ToolScope view; the worker
|
|
232
|
+
// RunScope IS a ToolScope (it extends it), so pass it directly — it returns a
|
|
233
|
+
// plain-data Record<string, unknown>. `sessionReplay` rebuilds the
|
|
234
|
+
// ToolSessionReplay from the stored row (`hookArg` is the serialized
|
|
235
|
+
// ToolSessionRecord the host read from the datastore).
|
|
236
|
+
const hookResult = spec.hook === 'collectReportData'
|
|
237
|
+
? await hooks.collectReportData?.(scope)
|
|
238
|
+
: hooks.sessionReplay?.replaySession(spec.hookArg);
|
|
239
|
+
return {
|
|
240
|
+
kind: 'result',
|
|
241
|
+
// `output` is required on the result; the host never replays output for a
|
|
242
|
+
// hook-mode dispatch (it reads `hookResult`), so a benign default suffices.
|
|
243
|
+
value: { output: 'command-result', ...(hookResult === undefined ? {} : { hookResult }) },
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* The testable core: produce the {@link DispatchWorkerMessage} the worker would
|
|
248
|
+
* post, without touching `process.send`. Never throws — every failure becomes a
|
|
249
|
+
* structured `error` message (the supervisor rejects on it). Must run inside an
|
|
250
|
+
* entered scope (the bootstrap enters it for the real subcommand; unit tests wrap
|
|
251
|
+
* it in `runWithScope`).
|
|
252
|
+
*/
|
|
253
|
+
export async function runToolCommandWorker(specPath) {
|
|
254
|
+
const spec = readSpec(specPath);
|
|
255
|
+
if ('kind' in spec)
|
|
256
|
+
return spec; // bad-spec error message
|
|
257
|
+
try {
|
|
258
|
+
return await runLoadedCommand(spec);
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
return errorMessage(error instanceof Error ? error.message : String(error), classifyThrow(error), error instanceof Error ? error.stack : undefined);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Run one external tool command headless in this worker and post the slim
|
|
266
|
+
* {@link ToolCommandResult} (or a structured `error`) over IPC. Never throws to
|
|
267
|
+
* the caller — every failure becomes an `error` IPC message so the supervisor
|
|
268
|
+
* rejects cleanly. This is the host CommandSpec handler's body.
|
|
269
|
+
*/
|
|
270
|
+
export async function executeToolCommandWorker(specPath) {
|
|
271
|
+
// @fitness-ignore-next-line detached-promises -- the promise IS awaited; `send(...)` is a synchronous void IPC post of the already-resolved value. The name-based heuristic misfires on `send(await ...)`.
|
|
272
|
+
send(await runToolCommandWorker(specPath));
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* `__tool-command-worker <specPath>` — the [internal] host subcommand the
|
|
276
|
+
* dispatch supervisor forks. Mirrors `graphRunWorkerCommandSpec`: `raw-stream`
|
|
277
|
+
* (it owns its own IPC output surface), `scope: 'project'` (the full bootstrap
|
|
278
|
+
* runs first), `visibility: 'internal'`. The supervisor passes `--cwd` so the
|
|
279
|
+
* bootstrap targets the right project. The handler ignores the host `ctx` it is
|
|
280
|
+
* given (the worker builds its OWN context shim over the bootstrapped scope) and
|
|
281
|
+
* posts the result over the IPC channel.
|
|
282
|
+
*/
|
|
283
|
+
export const toolCommandWorkerCommandSpec = defineCommand({
|
|
284
|
+
name: '__tool-command-worker',
|
|
285
|
+
visibility: 'internal',
|
|
286
|
+
description: '[internal] Run one external tool command headless in a forked worker and stream the result over IPC (forked by the ADR-0054 dispatch supervisor)',
|
|
287
|
+
// The supervisor passes `--cwd`; bootstrap uses it to resolve the project.
|
|
288
|
+
commonFlags: ['cwd'],
|
|
289
|
+
args: [{ name: 'specPath', description: 'Path to the JSON tool-command worker spec file' }],
|
|
290
|
+
scope: 'project',
|
|
291
|
+
output: 'raw-stream',
|
|
292
|
+
rawStreamReason: 'worker-ipc',
|
|
293
|
+
handler: async (rawOpts) => {
|
|
294
|
+
const specPath = rawOpts._args?.[0] ?? '';
|
|
295
|
+
await executeToolCommandWorker(specPath);
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
//# sourceMappingURL=tool-command-worker-entry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-command-worker-entry.js","sourceRoot":"","sources":["../../src/bootstrap/tool-command-worker-entry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tool-command-worker-rpc — the WORKER side of the ADR-0054 host-RPC upcall
|
|
3
|
+
* channel (increment M4-C).
|
|
4
|
+
*
|
|
5
|
+
* A host-RPC seam in the worker shim (`tool-command-worker-context.ts`) calls
|
|
6
|
+
* `hostRpc(request)`: it stamps a monotonic `rpcId`, posts the request to the
|
|
7
|
+
* parent over the transport's `progress` arm
|
|
8
|
+
* (`WorkerMessage<HostRpcRequest, ToolCommandResult>`), and BLOCKS on the
|
|
9
|
+
* matching {@link RpcReply} the parent posts back via `child.send`. The host
|
|
10
|
+
* performs the privileged effect through the real `ToolCliContext` and replies;
|
|
11
|
+
* a host-side fault crosses back as `{ ok: false, error }`, which this re-throws
|
|
12
|
+
* so the handler sees a normal thrown error (never a host crash, never a silent
|
|
13
|
+
* no-op).
|
|
14
|
+
*
|
|
15
|
+
* Replies are delivered on the worker's single `process.on('message')` listener,
|
|
16
|
+
* installed once and demultiplexed by `rpcId` into the pending-request map. The
|
|
17
|
+
* listener ignores anything that is not an `rpc-reply` (the channel is otherwise
|
|
18
|
+
* child → parent), so it never races the worker's own outbound posts.
|
|
19
|
+
*/
|
|
20
|
+
import type { HostRpcCall, HostRpcRequest } from './tool-command-dispatch-types.js';
|
|
21
|
+
import type { WorkerMessage } from '@opensip-cli/core';
|
|
22
|
+
/** The worker's outbound IPC type binding: requests stream on `progress`. */
|
|
23
|
+
type WorkerOutbound = WorkerMessage<HostRpcRequest, unknown>;
|
|
24
|
+
/**
|
|
25
|
+
* A worker RPC client: `call` issues one upcall and resolves with the host's
|
|
26
|
+
* reply value (or rejects with the host's structured error). `dispose` removes
|
|
27
|
+
* the reply listener (hygiene for the short-lived fork; the process exits on
|
|
28
|
+
* settle regardless).
|
|
29
|
+
*/
|
|
30
|
+
export interface WorkerRpcClient {
|
|
31
|
+
/**
|
|
32
|
+
* Issue one host-RPC upcall (sans `rpcId` — assigned here) and await the
|
|
33
|
+
* host's reply.
|
|
34
|
+
*
|
|
35
|
+
* @throws {Error} when the host reply is `{ ok: false }` (the host-side fault
|
|
36
|
+
* re-thrown into the handler), or when the worker is not forked with an IPC
|
|
37
|
+
* channel (no `process.send`, an integration-only misuse).
|
|
38
|
+
*/
|
|
39
|
+
readonly call: (request: HostRpcCall) => Promise<unknown>;
|
|
40
|
+
readonly dispose: () => void;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Create the worker's RPC client over the live IPC channel. Installs the single
|
|
44
|
+
* reply listener; `send` posts requests on the `progress` arm. The caller passes
|
|
45
|
+
* `process` so unit tests can supply a fake duplex.
|
|
46
|
+
*/
|
|
47
|
+
export declare function createWorkerRpcClient(channel: {
|
|
48
|
+
send?: (msg: WorkerOutbound) => unknown;
|
|
49
|
+
on: (event: 'message', listener: (msg: unknown) => void) => unknown;
|
|
50
|
+
off?: (event: 'message', listener: (msg: unknown) => void) => unknown;
|
|
51
|
+
}): WorkerRpcClient;
|
|
52
|
+
export {};
|
|
53
|
+
//# sourceMappingURL=tool-command-worker-rpc.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-command-worker-rpc.d.ts","sourceRoot":"","sources":["../../src/bootstrap/tool-command-worker-rpc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAY,MAAM,kCAAkC,CAAC;AAC9F,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD,6EAA6E;AAC7E,KAAK,cAAc,GAAG,aAAa,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC;AAE7D;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B;;;;;;;OAOG;IACH,QAAQ,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1D,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAC;CAC9B;AAyBD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE;IAC7C,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,OAAO,CAAC;IACxC,EAAE,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,KAAK,OAAO,CAAC;IACpE,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,KAAK,OAAO,CAAC;CACvE,GAAG,eAAe,CAuClB"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tool-command-worker-rpc — the WORKER side of the ADR-0054 host-RPC upcall
|
|
3
|
+
* channel (increment M4-C).
|
|
4
|
+
*
|
|
5
|
+
* A host-RPC seam in the worker shim (`tool-command-worker-context.ts`) calls
|
|
6
|
+
* `hostRpc(request)`: it stamps a monotonic `rpcId`, posts the request to the
|
|
7
|
+
* parent over the transport's `progress` arm
|
|
8
|
+
* (`WorkerMessage<HostRpcRequest, ToolCommandResult>`), and BLOCKS on the
|
|
9
|
+
* matching {@link RpcReply} the parent posts back via `child.send`. The host
|
|
10
|
+
* performs the privileged effect through the real `ToolCliContext` and replies;
|
|
11
|
+
* a host-side fault crosses back as `{ ok: false, error }`, which this re-throws
|
|
12
|
+
* so the handler sees a normal thrown error (never a host crash, never a silent
|
|
13
|
+
* no-op).
|
|
14
|
+
*
|
|
15
|
+
* Replies are delivered on the worker's single `process.on('message')` listener,
|
|
16
|
+
* installed once and demultiplexed by `rpcId` into the pending-request map. The
|
|
17
|
+
* listener ignores anything that is not an `rpc-reply` (the channel is otherwise
|
|
18
|
+
* child → parent), so it never races the worker's own outbound posts.
|
|
19
|
+
*/
|
|
20
|
+
/** Reconstruct an Error from a host reply's structured error (preserving stack). */
|
|
21
|
+
function rpcError(detail) {
|
|
22
|
+
const err = new Error(detail.message);
|
|
23
|
+
if (detail.code !== undefined)
|
|
24
|
+
err.code = detail.code;
|
|
25
|
+
if (detail.stack !== undefined)
|
|
26
|
+
err.stack = detail.stack;
|
|
27
|
+
return err;
|
|
28
|
+
}
|
|
29
|
+
/** Narrow an arbitrary IPC message to a {@link RpcReply}. */
|
|
30
|
+
function isRpcReply(msg) {
|
|
31
|
+
return (typeof msg === 'object' &&
|
|
32
|
+
msg !== null &&
|
|
33
|
+
msg.kind === 'rpc-reply' &&
|
|
34
|
+
typeof msg.rpcId === 'number');
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Create the worker's RPC client over the live IPC channel. Installs the single
|
|
38
|
+
* reply listener; `send` posts requests on the `progress` arm. The caller passes
|
|
39
|
+
* `process` so unit tests can supply a fake duplex.
|
|
40
|
+
*/
|
|
41
|
+
export function createWorkerRpcClient(channel) {
|
|
42
|
+
const pending = new Map();
|
|
43
|
+
let nextId = 1;
|
|
44
|
+
const onMessage = (msg) => {
|
|
45
|
+
if (!isRpcReply(msg))
|
|
46
|
+
return; // not a reply — the channel is otherwise outbound
|
|
47
|
+
const waiter = pending.get(msg.rpcId);
|
|
48
|
+
if (waiter === undefined)
|
|
49
|
+
return; // unknown/duplicate rpcId — defensively ignore
|
|
50
|
+
pending.delete(msg.rpcId);
|
|
51
|
+
if (msg.ok)
|
|
52
|
+
waiter.resolve(msg.value);
|
|
53
|
+
else
|
|
54
|
+
waiter.reject(rpcError(msg.error));
|
|
55
|
+
};
|
|
56
|
+
channel.on('message', onMessage);
|
|
57
|
+
return {
|
|
58
|
+
call: (request) => new Promise((resolve, reject) => {
|
|
59
|
+
if (channel.send === undefined) {
|
|
60
|
+
reject(new Error('tool command worker: no IPC channel to issue a host-RPC upcall ' +
|
|
61
|
+
`('${request.seam}') — the worker must be forked with an 'ipc' stdio channel.`));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const rpcId = nextId++;
|
|
65
|
+
pending.set(rpcId, { resolve, reject });
|
|
66
|
+
const event = { ...request, rpcId };
|
|
67
|
+
// Call through `channel.send(...)` (not an extracted reference) so the
|
|
68
|
+
// method keeps its `this` binding — `process.send` reads `this.connected`
|
|
69
|
+
// internally and throws if invoked unbound.
|
|
70
|
+
channel.send({ kind: 'progress', event });
|
|
71
|
+
}),
|
|
72
|
+
dispose: () => {
|
|
73
|
+
channel.off?.('message', onMessage);
|
|
74
|
+
pending.clear();
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=tool-command-worker-rpc.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-command-worker-rpc.js","sourceRoot":"","sources":["../../src/bootstrap/tool-command-worker-rpc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAgCH,oFAAoF;AACpF,SAAS,QAAQ,CAAC,MAA0D;IAC1E,MAAM,GAAG,GAAG,IAAI,KAAK,CAAC,MAAM,CAAC,OAAO,CAA8B,CAAC;IACnE,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS;QAAE,GAAG,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;IACtD,IAAI,MAAM,CAAC,KAAK,KAAK,SAAS;QAAE,GAAG,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;IACzD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,6DAA6D;AAC7D,SAAS,UAAU,CAAC,GAAY;IAC9B,OAAO,CACL,OAAO,GAAG,KAAK,QAAQ;QACvB,GAAG,KAAK,IAAI;QACX,GAA0B,CAAC,IAAI,KAAK,WAAW;QAChD,OAAQ,GAA2B,CAAC,KAAK,KAAK,QAAQ,CACvD,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,OAIrC;IACC,MAAM,OAAO,GAAG,IAAI,GAAG,EAAuB,CAAC;IAC/C,IAAI,MAAM,GAAG,CAAC,CAAC;IAEf,MAAM,SAAS,GAAG,CAAC,GAAY,EAAQ,EAAE;QACvC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,OAAO,CAAC,kDAAkD;QAChF,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACtC,IAAI,MAAM,KAAK,SAAS;YAAE,OAAO,CAAC,+CAA+C;QACjF,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAC1B,IAAI,GAAG,CAAC,EAAE;YAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;;YACjC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC;IAC1C,CAAC,CAAC;IACF,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;IAEjC,OAAO;QACL,IAAI,EAAE,CAAC,OAAO,EAAE,EAAE,CAChB,IAAI,OAAO,CAAU,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACvC,IAAI,OAAO,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;gBAC/B,MAAM,CACJ,IAAI,KAAK,CACP,iEAAiE;oBAC/D,KAAK,OAAO,CAAC,IAAI,6DAA6D,CACjF,CACF,CAAC;gBACF,OAAO;YACT,CAAC;YACD,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC;YACvB,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;YACxC,MAAM,KAAK,GAAmB,EAAE,GAAG,OAAO,EAAE,KAAK,EAAE,CAAC;YACpD,uEAAuE;YACvE,0EAA0E;YAC1E,4CAA4C;YAC5C,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC;QAC5C,CAAC,CAAC;QACJ,OAAO,EAAE,GAAG,EAAE;YACZ,OAAO,CAAC,GAAG,EAAE,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;YACpC,OAAO,CAAC,KAAK,EAAE,CAAC;QAClB,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tool-provenance — the shared per-run provenance classifier + the host/worker
|
|
3
|
+
* hook-execution gate (ADR-0054 M4-F).
|
|
4
|
+
*
|
|
5
|
+
* Three sites resolve a tool's `ToolProvenance.source` from the per-run
|
|
6
|
+
* provenance array (recorded by the bootstrap, matched by stable id then human
|
|
7
|
+
* name): `bind-external-dispatch.ts` (dispatch gate), `config-and-capabilities.ts`
|
|
8
|
+
* (config two-pass fold + capability wiring), and now the lifecycle host loops
|
|
9
|
+
* (contributeScope / initialize / report / replay). This module is the ONE matcher
|
|
10
|
+
* so they never drift.
|
|
11
|
+
*
|
|
12
|
+
* It also owns the **host-vs-worker** gate that makes the M4-F lifecycle skip
|
|
13
|
+
* correct. M4-F stops the HOST from executing external-provenance lifecycle +
|
|
14
|
+
* capability hooks — but the dispatch WORKER re-runs the SAME bootstrap code
|
|
15
|
+
* (`build-per-run-scope` / `config-and-capabilities`) and MUST run the dispatched
|
|
16
|
+
* external tool's hooks there (the worker is the legitimate isolation boundary —
|
|
17
|
+
* that is the whole point: external runtime executes in the worker, not the host).
|
|
18
|
+
*
|
|
19
|
+
* So the skip is gated on "this process is the HOST, not a dispatch worker." The
|
|
20
|
+
* supervisor sets {@link IN_TOOL_WORKER_ENV} on the forked child's env; inside the
|
|
21
|
+
* worker {@link isExternalHookHostSkipActive} returns `false`, so the worker runs
|
|
22
|
+
* ALL its registered tools' hooks worker-local exactly as a normal in-process
|
|
23
|
+
* bootstrap would. In the host it returns `true`, so external hooks are skipped.
|
|
24
|
+
*/
|
|
25
|
+
import { type Tool, type ToolProvenance, type ToolSource } from '@opensip-cli/core';
|
|
26
|
+
/**
|
|
27
|
+
* Env flag the dispatch supervisor sets on the forked `__tool-command-worker`
|
|
28
|
+
* child so the worker can tell it is the isolation boundary (NOT the host). The
|
|
29
|
+
* worker bootstrap runs the SAME lifecycle loops as the host; this flag disables
|
|
30
|
+
* the M4-F external-hook host-skip inside the worker so the dispatched tool's
|
|
31
|
+
* hooks run there. A separate process + env channel (symmetric to OPENSIP_RUN_ID
|
|
32
|
+
* injection) keeps it deterministic and unit-testable.
|
|
33
|
+
*/
|
|
34
|
+
export declare const IN_TOOL_WORKER_ENV = "OPENSIP_CLI_IN_TOOL_WORKER";
|
|
35
|
+
/**
|
|
36
|
+
* Is the M4-F external-hook host-skip ACTIVE in this process? `true` in the host
|
|
37
|
+
* (skip external hooks), `false` inside a dispatch worker (run them — the worker
|
|
38
|
+
* is the isolation boundary). Reads the injected env (defaults to `process.env`)
|
|
39
|
+
* so tests can flip it deterministically without a fork.
|
|
40
|
+
*/
|
|
41
|
+
export declare function isExternalHookHostSkipActive(env?: NodeJS.ProcessEnv): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Must this process REFUSE to import external tool runtimes (ADR-0054 M4-G
|
|
44
|
+
* capstone)? `true` in the HOST — the host registers a manifest-derived synthetic
|
|
45
|
+
* Tool for external provenance and NEVER loads its runtime; `false` inside a
|
|
46
|
+
* dispatch worker (`OPENSIP_CLI_IN_TOOL_WORKER=1`) — the worker IS the isolation
|
|
47
|
+
* boundary, where the untrusted external runtime legitimately loads + runs.
|
|
48
|
+
*
|
|
49
|
+
* This is the same host-vs-worker hinge as the M4-F lifecycle gate (the worker
|
|
50
|
+
* re-runs the SAME bootstrap, so discovery must branch on which process it is),
|
|
51
|
+
* named for the capstone's discovery decision. Reads the injected env (defaults
|
|
52
|
+
* to `process.env`) so tests flip it deterministically without a fork.
|
|
53
|
+
*/
|
|
54
|
+
export declare function isHostRuntimeImportForbidden(env?: NodeJS.ProcessEnv): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Resolve a tool's provenance source from the per-run provenance array. Mirrors
|
|
57
|
+
* the dispatch/config matchers: prefer the stable-id match (UUID → `metadata.id`),
|
|
58
|
+
* fall back to the human key (`ToolProvenance.id` → `metadata.name`). A tool with
|
|
59
|
+
* NO recorded provenance is the trusted/unknown path → `'bundled'` semantics (its
|
|
60
|
+
* hooks run in-host, exactly as before M4-F).
|
|
61
|
+
*/
|
|
62
|
+
export declare function provenanceSourceFor(tool: Tool, provenance: readonly ToolProvenance[]): ToolSource;
|
|
63
|
+
/** Find the full provenance record for a tool (stable-id then human-name match). */
|
|
64
|
+
export declare function provenanceRecordFor(tool: Tool, provenance: readonly ToolProvenance[]): ToolProvenance | undefined;
|
|
65
|
+
/**
|
|
66
|
+
* Is this tool external-provenance (installed / project-local / user-global)? A
|
|
67
|
+
* tool with no recorded provenance is treated as bundled (trusted/unknown path).
|
|
68
|
+
*/
|
|
69
|
+
export declare function isExternalToolProvenance(tool: Tool, provenance: readonly ToolProvenance[]): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* The M4-F gate: should this tool's lifecycle/capability hook run IN THE HOST
|
|
72
|
+
* process this invocation?
|
|
73
|
+
*
|
|
74
|
+
* - Bundled (or no provenance recorded) → `true` (trusted computing base; runs
|
|
75
|
+
* in-host exactly as before M4-F).
|
|
76
|
+
* - External, host-skip ACTIVE (we are the host) → `false` (the host never
|
|
77
|
+
* executes external runtime hooks; they run worker-side).
|
|
78
|
+
* - External, host-skip INACTIVE (we are inside a dispatch worker) → `true`
|
|
79
|
+
* (the worker IS the isolation boundary — run the dispatched tool's hooks
|
|
80
|
+
* worker-local).
|
|
81
|
+
*
|
|
82
|
+
* @param env Injected env (defaults to `process.env`) for the worker-flag read.
|
|
83
|
+
*/
|
|
84
|
+
export declare function shouldRunHookInHost(tool: Tool, provenance: readonly ToolProvenance[], env?: NodeJS.ProcessEnv): boolean;
|
|
85
|
+
//# sourceMappingURL=tool-provenance.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-provenance.d.ts","sourceRoot":"","sources":["../../src/bootstrap/tool-provenance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,KAAK,IAAI,EAAE,KAAK,cAAc,EAAE,KAAK,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpF;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,+BAA+B,CAAC;AAE/D;;;;;GAKG;AACH,wBAAgB,4BAA4B,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAE1F;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,4BAA4B,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAE1F;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,SAAS,cAAc,EAAE,GAAG,UAAU,CAKjG;AAED,oFAAoF;AACpF,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,IAAI,EACV,UAAU,EAAE,SAAS,cAAc,EAAE,GACpC,cAAc,GAAG,SAAS,CAK5B;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,IAAI,EACV,UAAU,EAAE,SAAS,cAAc,EAAE,GACpC,OAAO,CAET;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,IAAI,EACV,UAAU,EAAE,SAAS,cAAc,EAAE,EACrC,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,OAAO,CAGT"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tool-provenance — the shared per-run provenance classifier + the host/worker
|
|
3
|
+
* hook-execution gate (ADR-0054 M4-F).
|
|
4
|
+
*
|
|
5
|
+
* Three sites resolve a tool's `ToolProvenance.source` from the per-run
|
|
6
|
+
* provenance array (recorded by the bootstrap, matched by stable id then human
|
|
7
|
+
* name): `bind-external-dispatch.ts` (dispatch gate), `config-and-capabilities.ts`
|
|
8
|
+
* (config two-pass fold + capability wiring), and now the lifecycle host loops
|
|
9
|
+
* (contributeScope / initialize / report / replay). This module is the ONE matcher
|
|
10
|
+
* so they never drift.
|
|
11
|
+
*
|
|
12
|
+
* It also owns the **host-vs-worker** gate that makes the M4-F lifecycle skip
|
|
13
|
+
* correct. M4-F stops the HOST from executing external-provenance lifecycle +
|
|
14
|
+
* capability hooks — but the dispatch WORKER re-runs the SAME bootstrap code
|
|
15
|
+
* (`build-per-run-scope` / `config-and-capabilities`) and MUST run the dispatched
|
|
16
|
+
* external tool's hooks there (the worker is the legitimate isolation boundary —
|
|
17
|
+
* that is the whole point: external runtime executes in the worker, not the host).
|
|
18
|
+
*
|
|
19
|
+
* So the skip is gated on "this process is the HOST, not a dispatch worker." The
|
|
20
|
+
* supervisor sets {@link IN_TOOL_WORKER_ENV} on the forked child's env; inside the
|
|
21
|
+
* worker {@link isExternalHookHostSkipActive} returns `false`, so the worker runs
|
|
22
|
+
* ALL its registered tools' hooks worker-local exactly as a normal in-process
|
|
23
|
+
* bootstrap would. In the host it returns `true`, so external hooks are skipped.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Env flag the dispatch supervisor sets on the forked `__tool-command-worker`
|
|
27
|
+
* child so the worker can tell it is the isolation boundary (NOT the host). The
|
|
28
|
+
* worker bootstrap runs the SAME lifecycle loops as the host; this flag disables
|
|
29
|
+
* the M4-F external-hook host-skip inside the worker so the dispatched tool's
|
|
30
|
+
* hooks run there. A separate process + env channel (symmetric to OPENSIP_RUN_ID
|
|
31
|
+
* injection) keeps it deterministic and unit-testable.
|
|
32
|
+
*/
|
|
33
|
+
export const IN_TOOL_WORKER_ENV = 'OPENSIP_CLI_IN_TOOL_WORKER';
|
|
34
|
+
/**
|
|
35
|
+
* Is the M4-F external-hook host-skip ACTIVE in this process? `true` in the host
|
|
36
|
+
* (skip external hooks), `false` inside a dispatch worker (run them — the worker
|
|
37
|
+
* is the isolation boundary). Reads the injected env (defaults to `process.env`)
|
|
38
|
+
* so tests can flip it deterministically without a fork.
|
|
39
|
+
*/
|
|
40
|
+
export function isExternalHookHostSkipActive(env = process.env) {
|
|
41
|
+
return env[IN_TOOL_WORKER_ENV] !== '1';
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Must this process REFUSE to import external tool runtimes (ADR-0054 M4-G
|
|
45
|
+
* capstone)? `true` in the HOST — the host registers a manifest-derived synthetic
|
|
46
|
+
* Tool for external provenance and NEVER loads its runtime; `false` inside a
|
|
47
|
+
* dispatch worker (`OPENSIP_CLI_IN_TOOL_WORKER=1`) — the worker IS the isolation
|
|
48
|
+
* boundary, where the untrusted external runtime legitimately loads + runs.
|
|
49
|
+
*
|
|
50
|
+
* This is the same host-vs-worker hinge as the M4-F lifecycle gate (the worker
|
|
51
|
+
* re-runs the SAME bootstrap, so discovery must branch on which process it is),
|
|
52
|
+
* named for the capstone's discovery decision. Reads the injected env (defaults
|
|
53
|
+
* to `process.env`) so tests flip it deterministically without a fork.
|
|
54
|
+
*/
|
|
55
|
+
export function isHostRuntimeImportForbidden(env = process.env) {
|
|
56
|
+
return isExternalHookHostSkipActive(env);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Resolve a tool's provenance source from the per-run provenance array. Mirrors
|
|
60
|
+
* the dispatch/config matchers: prefer the stable-id match (UUID → `metadata.id`),
|
|
61
|
+
* fall back to the human key (`ToolProvenance.id` → `metadata.name`). A tool with
|
|
62
|
+
* NO recorded provenance is the trusted/unknown path → `'bundled'` semantics (its
|
|
63
|
+
* hooks run in-host, exactly as before M4-F).
|
|
64
|
+
*/
|
|
65
|
+
export function provenanceSourceFor(tool, provenance) {
|
|
66
|
+
const recorded = provenance.find((p) => p.stableId !== undefined && p.stableId === tool.metadata.id) ??
|
|
67
|
+
provenance.find((p) => p.id === tool.metadata.name);
|
|
68
|
+
return recorded?.source ?? 'bundled';
|
|
69
|
+
}
|
|
70
|
+
/** Find the full provenance record for a tool (stable-id then human-name match). */
|
|
71
|
+
export function provenanceRecordFor(tool, provenance) {
|
|
72
|
+
return (provenance.find((p) => p.stableId !== undefined && p.stableId === tool.metadata.id) ??
|
|
73
|
+
provenance.find((p) => p.id === tool.metadata.name));
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Is this tool external-provenance (installed / project-local / user-global)? A
|
|
77
|
+
* tool with no recorded provenance is treated as bundled (trusted/unknown path).
|
|
78
|
+
*/
|
|
79
|
+
export function isExternalToolProvenance(tool, provenance) {
|
|
80
|
+
return provenanceSourceFor(tool, provenance) !== 'bundled';
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The M4-F gate: should this tool's lifecycle/capability hook run IN THE HOST
|
|
84
|
+
* process this invocation?
|
|
85
|
+
*
|
|
86
|
+
* - Bundled (or no provenance recorded) → `true` (trusted computing base; runs
|
|
87
|
+
* in-host exactly as before M4-F).
|
|
88
|
+
* - External, host-skip ACTIVE (we are the host) → `false` (the host never
|
|
89
|
+
* executes external runtime hooks; they run worker-side).
|
|
90
|
+
* - External, host-skip INACTIVE (we are inside a dispatch worker) → `true`
|
|
91
|
+
* (the worker IS the isolation boundary — run the dispatched tool's hooks
|
|
92
|
+
* worker-local).
|
|
93
|
+
*
|
|
94
|
+
* @param env Injected env (defaults to `process.env`) for the worker-flag read.
|
|
95
|
+
*/
|
|
96
|
+
export function shouldRunHookInHost(tool, provenance, env = process.env) {
|
|
97
|
+
if (!isExternalToolProvenance(tool, provenance))
|
|
98
|
+
return true;
|
|
99
|
+
return !isExternalHookHostSkipActive(env);
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=tool-provenance.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-provenance.js","sourceRoot":"","sources":["../../src/bootstrap/tool-provenance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAIH;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,4BAA4B,CAAC;AAE/D;;;;;GAKG;AACH,MAAM,UAAU,4BAA4B,CAAC,MAAyB,OAAO,CAAC,GAAG;IAC/E,OAAO,GAAG,CAAC,kBAAkB,CAAC,KAAK,GAAG,CAAC;AACzC,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,4BAA4B,CAAC,MAAyB,OAAO,CAAC,GAAG;IAC/E,OAAO,4BAA4B,CAAC,GAAG,CAAC,CAAC;AAC3C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAU,EAAE,UAAqC;IACnF,MAAM,QAAQ,GACZ,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,SAAS,IAAI,CAAC,CAAC,QAAQ,KAAK,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QACnF,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IACtD,OAAO,QAAQ,EAAE,MAAM,IAAI,SAAS,CAAC;AACvC,CAAC;AAED,oFAAoF;AACpF,MAAM,UAAU,mBAAmB,CACjC,IAAU,EACV,UAAqC;IAErC,OAAO,CACL,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,SAAS,IAAI,CAAC,CAAC,QAAQ,KAAK,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QACnF,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CACpD,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,wBAAwB,CACtC,IAAU,EACV,UAAqC;IAErC,OAAO,mBAAmB,CAAC,IAAI,EAAE,UAAU,CAAC,KAAK,SAAS,CAAC;AAC7D,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,mBAAmB,CACjC,IAAU,EACV,UAAqC,EACrC,MAAyB,OAAO,CAAC,GAAG;IAEpC,IAAI,CAAC,wBAAwB,CAAC,IAAI,EAAE,UAAU,CAAC;QAAE,OAAO,IAAI,CAAC;IAC7D,OAAO,CAAC,4BAA4B,CAAC,GAAG,CAAC,CAAC;AAC5C,CAAC"}
|