agentfootprint 11.4.0 → 12.0.0
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/CAPABILITIES.md +3 -1
- package/dist/adapters/code/agentcore.js +295 -214
- package/dist/adapters/code/agentcore.js.map +1 -1
- package/dist/adapters/code/agentcoreClient.js +178 -0
- package/dist/adapters/code/agentcoreClient.js.map +1 -0
- package/dist/adapters/code/agentcoreWire.js +348 -0
- package/dist/adapters/code/agentcoreWire.js.map +1 -0
- package/dist/adapters/code/fileName.js +100 -0
- package/dist/adapters/code/fileName.js.map +1 -0
- package/dist/adapters/code/local.js +529 -183
- package/dist/adapters/code/local.js.map +1 -1
- package/dist/adapters/code/output.js +140 -0
- package/dist/adapters/code/output.js.map +1 -0
- package/dist/adapters/mcp/agentcoreRuntime/invoke.js +7 -6
- package/dist/adapters/mcp/agentcoreRuntime/invoke.js.map +1 -1
- package/dist/adapters/mcp/agentcoreRuntime/sessionBusy.js +36 -2
- package/dist/adapters/mcp/agentcoreRuntime/sessionBusy.js.map +1 -1
- package/dist/adapters/mcp/agentcoreRuntime/transport.js +3 -1
- package/dist/adapters/mcp/agentcoreRuntime/transport.js.map +1 -1
- package/dist/core/codeRunnerTool.js +52 -7
- package/dist/core/codeRunnerTool.js.map +1 -1
- package/dist/doors/providers.js.map +1 -1
- package/dist/esm/adapters/code/agentcore.d.ts +65 -68
- package/dist/esm/adapters/code/agentcore.js +293 -212
- package/dist/esm/adapters/code/agentcore.js.map +1 -1
- package/dist/esm/adapters/code/agentcoreClient.d.ts +193 -0
- package/dist/esm/adapters/code/agentcoreClient.js +173 -0
- package/dist/esm/adapters/code/agentcoreClient.js.map +1 -0
- package/dist/esm/adapters/code/agentcoreWire.d.ts +119 -0
- package/dist/esm/adapters/code/agentcoreWire.js +334 -0
- package/dist/esm/adapters/code/agentcoreWire.js.map +1 -0
- package/dist/esm/adapters/code/fileName.d.ts +43 -0
- package/dist/esm/adapters/code/fileName.js +96 -0
- package/dist/esm/adapters/code/fileName.js.map +1 -0
- package/dist/esm/adapters/code/local.d.ts +91 -12
- package/dist/esm/adapters/code/local.js +528 -182
- package/dist/esm/adapters/code/local.js.map +1 -1
- package/dist/esm/adapters/code/output.d.ts +70 -0
- package/dist/esm/adapters/code/output.js +129 -0
- package/dist/esm/adapters/code/output.js.map +1 -0
- package/dist/esm/adapters/mcp/agentcoreRuntime/invoke.d.ts +7 -6
- package/dist/esm/adapters/mcp/agentcoreRuntime/invoke.js +7 -6
- package/dist/esm/adapters/mcp/agentcoreRuntime/invoke.js.map +1 -1
- package/dist/esm/adapters/mcp/agentcoreRuntime/sessionBusy.d.ts +32 -1
- package/dist/esm/adapters/mcp/agentcoreRuntime/sessionBusy.js +35 -1
- package/dist/esm/adapters/mcp/agentcoreRuntime/sessionBusy.js.map +1 -1
- package/dist/esm/adapters/mcp/agentcoreRuntime/transport.js +4 -2
- package/dist/esm/adapters/mcp/agentcoreRuntime/transport.js.map +1 -1
- package/dist/esm/core/codeRunnerTool.d.ts +15 -3
- package/dist/esm/core/codeRunnerTool.js +52 -7
- package/dist/esm/core/codeRunnerTool.js.map +1 -1
- package/dist/esm/doors/providers.d.ts +1 -1
- package/dist/esm/doors/providers.js.map +1 -1
- package/dist/esm/events/payloads.d.ts +144 -29
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +5 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/mcp/callLifetime.d.ts +57 -14
- package/dist/esm/mcp/callLifetime.js +67 -19
- package/dist/esm/mcp/callLifetime.js.map +1 -1
- package/dist/esm/mcp/callMarker.d.ts +43 -0
- package/dist/esm/mcp/callMarker.js +69 -0
- package/dist/esm/mcp/callMarker.js.map +1 -0
- package/dist/esm/mcp/connectionRefusals.js +7 -0
- package/dist/esm/mcp/connectionRefusals.js.map +1 -1
- package/dist/esm/mcp/index.d.ts +2 -0
- package/dist/esm/mcp/index.js +3 -0
- package/dist/esm/mcp/index.js.map +1 -1
- package/dist/esm/mcp/mcpClient.js +93 -26
- package/dist/esm/mcp/mcpClient.js.map +1 -1
- package/dist/esm/mcp/replay.d.ts +291 -0
- package/dist/esm/mcp/replay.js +405 -0
- package/dist/esm/mcp/replay.js.map +1 -0
- package/dist/esm/mcp/replayUnavailable.d.ts +68 -0
- package/dist/esm/mcp/replayUnavailable.js +87 -0
- package/dist/esm/mcp/replayUnavailable.js.map +1 -0
- package/dist/esm/mcp/requestLedger.d.ts +115 -32
- package/dist/esm/mcp/requestLedger.js +193 -52
- package/dist/esm/mcp/requestLedger.js.map +1 -1
- package/dist/esm/mcp/seriesEvidence.d.ts +73 -15
- package/dist/esm/mcp/seriesEvidence.js +114 -21
- package/dist/esm/mcp/seriesEvidence.js.map +1 -1
- package/dist/esm/mcp/throttleRetry.d.ts +86 -42
- package/dist/esm/mcp/throttleRetry.js +158 -50
- package/dist/esm/mcp/throttleRetry.js.map +1 -1
- package/dist/esm/mcp/toolResult.d.ts +6 -0
- package/dist/esm/mcp/toolResult.js +15 -0
- package/dist/esm/mcp/toolResult.js.map +1 -1
- package/dist/esm/mcp/types.d.ts +73 -8
- package/dist/esm/provider-contract/codeSandbox.d.ts +89 -0
- package/dist/esm/provider-contract/codeSandbox.js +119 -0
- package/dist/esm/provider-contract/codeSandbox.js.map +1 -0
- package/dist/esm/provider-contract/sessionExpired.d.ts +31 -0
- package/dist/esm/provider-contract/sessionExpired.js +40 -0
- package/dist/esm/provider-contract/sessionExpired.js.map +1 -0
- package/dist/esm/provider-contract/types.d.ts +166 -17
- package/dist/esm/provider-contract/types.js +9 -0
- package/dist/esm/provider-contract/types.js.map +1 -1
- package/dist/esm/redaction/knownStrings.js +16 -0
- package/dist/esm/redaction/knownStrings.js.map +1 -1
- package/dist/esm/tool-providers/index.d.ts +2 -2
- package/dist/esm/tool-providers/index.js +4 -0
- package/dist/esm/tool-providers/index.js.map +1 -1
- package/dist/esm/tools/requestEvidence.js +60 -0
- package/dist/esm/tools/requestEvidence.js.map +1 -1
- package/dist/index.js +12 -4
- package/dist/index.js.map +1 -1
- package/dist/mcp/callLifetime.js +66 -18
- package/dist/mcp/callLifetime.js.map +1 -1
- package/dist/mcp/callMarker.js +74 -0
- package/dist/mcp/callMarker.js.map +1 -0
- package/dist/mcp/connectionRefusals.js +7 -0
- package/dist/mcp/connectionRefusals.js.map +1 -1
- package/dist/mcp/index.js +5 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/mcpClient.js +91 -24
- package/dist/mcp/mcpClient.js.map +1 -1
- package/dist/mcp/replay.js +415 -0
- package/dist/mcp/replay.js.map +1 -0
- package/dist/mcp/replayUnavailable.js +92 -0
- package/dist/mcp/replayUnavailable.js.map +1 -0
- package/dist/mcp/requestLedger.js +193 -52
- package/dist/mcp/requestLedger.js.map +1 -1
- package/dist/mcp/seriesEvidence.js +116 -22
- package/dist/mcp/seriesEvidence.js.map +1 -1
- package/dist/mcp/throttleRetry.js +160 -51
- package/dist/mcp/throttleRetry.js.map +1 -1
- package/dist/mcp/toolResult.js +17 -1
- package/dist/mcp/toolResult.js.map +1 -1
- package/dist/provider-contract/codeSandbox.js +125 -0
- package/dist/provider-contract/codeSandbox.js.map +1 -0
- package/dist/provider-contract/sessionExpired.js +44 -0
- package/dist/provider-contract/sessionExpired.js.map +1 -0
- package/dist/provider-contract/types.js +12 -1
- package/dist/provider-contract/types.js.map +1 -1
- package/dist/redaction/knownStrings.js +16 -0
- package/dist/redaction/knownStrings.js.map +1 -1
- package/dist/tool-providers/index.js +5 -1
- package/dist/tool-providers/index.js.map +1 -1
- package/dist/tools/requestEvidence.js +60 -0
- package/dist/tools/requestEvidence.js.map +1 -1
- package/dist/types/adapters/code/agentcore.d.ts +65 -68
- package/dist/types/adapters/code/agentcore.d.ts.map +1 -1
- package/dist/types/adapters/code/agentcoreClient.d.ts +194 -0
- package/dist/types/adapters/code/agentcoreClient.d.ts.map +1 -0
- package/dist/types/adapters/code/agentcoreWire.d.ts +120 -0
- package/dist/types/adapters/code/agentcoreWire.d.ts.map +1 -0
- package/dist/types/adapters/code/fileName.d.ts +44 -0
- package/dist/types/adapters/code/fileName.d.ts.map +1 -0
- package/dist/types/adapters/code/local.d.ts +91 -12
- package/dist/types/adapters/code/local.d.ts.map +1 -1
- package/dist/types/adapters/code/output.d.ts +71 -0
- package/dist/types/adapters/code/output.d.ts.map +1 -0
- package/dist/types/adapters/mcp/agentcoreRuntime/invoke.d.ts +7 -6
- package/dist/types/adapters/mcp/agentcoreRuntime/invoke.d.ts.map +1 -1
- package/dist/types/adapters/mcp/agentcoreRuntime/sessionBusy.d.ts +32 -1
- package/dist/types/adapters/mcp/agentcoreRuntime/sessionBusy.d.ts.map +1 -1
- package/dist/types/adapters/mcp/agentcoreRuntime/transport.d.ts.map +1 -1
- package/dist/types/core/codeRunnerTool.d.ts +15 -3
- package/dist/types/core/codeRunnerTool.d.ts.map +1 -1
- package/dist/types/doors/providers.d.ts +1 -1
- package/dist/types/doors/providers.d.ts.map +1 -1
- package/dist/types/events/payloads.d.ts +144 -29
- package/dist/types/events/payloads.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/mcp/callLifetime.d.ts +57 -14
- package/dist/types/mcp/callLifetime.d.ts.map +1 -1
- package/dist/types/mcp/callMarker.d.ts +44 -0
- package/dist/types/mcp/callMarker.d.ts.map +1 -0
- package/dist/types/mcp/index.d.ts +2 -0
- package/dist/types/mcp/index.d.ts.map +1 -1
- package/dist/types/mcp/mcpClient.d.ts.map +1 -1
- package/dist/types/mcp/replay.d.ts +292 -0
- package/dist/types/mcp/replay.d.ts.map +1 -0
- package/dist/types/mcp/replayUnavailable.d.ts +69 -0
- package/dist/types/mcp/replayUnavailable.d.ts.map +1 -0
- package/dist/types/mcp/requestLedger.d.ts +115 -32
- package/dist/types/mcp/requestLedger.d.ts.map +1 -1
- package/dist/types/mcp/seriesEvidence.d.ts +73 -15
- package/dist/types/mcp/seriesEvidence.d.ts.map +1 -1
- package/dist/types/mcp/throttleRetry.d.ts +86 -42
- package/dist/types/mcp/throttleRetry.d.ts.map +1 -1
- package/dist/types/mcp/toolResult.d.ts +6 -0
- package/dist/types/mcp/toolResult.d.ts.map +1 -1
- package/dist/types/mcp/types.d.ts +73 -8
- package/dist/types/mcp/types.d.ts.map +1 -1
- package/dist/types/provider-contract/codeSandbox.d.ts +90 -0
- package/dist/types/provider-contract/codeSandbox.d.ts.map +1 -0
- package/dist/types/provider-contract/sessionExpired.d.ts +32 -0
- package/dist/types/provider-contract/sessionExpired.d.ts.map +1 -0
- package/dist/types/provider-contract/types.d.ts +166 -17
- package/dist/types/provider-contract/types.d.ts.map +1 -1
- package/dist/types/redaction/knownStrings.d.ts.map +1 -1
- package/dist/types/tool-providers/index.d.ts +2 -2
- package/dist/types/tool-providers/index.d.ts.map +1 -1
- package/dist/types/tools/requestEvidence.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -13,7 +13,13 @@
|
|
|
13
13
|
*
|
|
14
14
|
* What a child process DOES give you:
|
|
15
15
|
* • a separate process and heap — a crash or an OOM takes the child, not you;
|
|
16
|
-
* • kill on timeout, so runaway code has a ceiling
|
|
16
|
+
* • kill on timeout, so runaway code has a ceiling — on POSIX the child
|
|
17
|
+
* runs in a process group of its own, and at the timeout, on an abort or
|
|
18
|
+
* at the output ceiling the runner stops that OWNED group (SIGKILL). That
|
|
19
|
+
* is cleanup, not containment: a descendant that starts its own session
|
|
20
|
+
* or group escapes it, and a host killed before its exit handlers run
|
|
21
|
+
* stops nothing. On Windows only the child itself can be stopped, and the
|
|
22
|
+
* timeout error says so;
|
|
17
23
|
* • no inherited stdin, so nothing can block waiting for a terminal;
|
|
18
24
|
* • an environment ALLOWLIST — `process.env` is not inherited (only `PATH`,
|
|
19
25
|
* so the interpreter can be found), so an `AWS_SECRET_ACCESS_KEY` in your
|
|
@@ -35,11 +41,21 @@
|
|
|
35
41
|
* is not a set this adapter can identify without guessing which of a developer's
|
|
36
42
|
* files were meant, and a dev-loop runner that quietly uploaded whatever
|
|
37
43
|
* appeared beside your source would be the worse failure. Producing outputs
|
|
38
|
-
* needs
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
44
|
+
* needs a declared output directory the model is told about and a size policy
|
|
45
|
+
* for what is too big to carry in-band; `readFile` below is the bounded
|
|
46
|
+
* read-back, for a caller that names the file. A runner that has all three
|
|
47
|
+
* fills the field, and `codeRunnerTool` mints every data-carrying entry with
|
|
48
|
+
* no further wiring.
|
|
49
|
+
*
|
|
50
|
+
* ── Files and commands ─────────────────────────────────────────────────────
|
|
51
|
+
* `readFile(path, { maxBytes })` reads ONE file back from the working
|
|
52
|
+
* directory — the path checked by the port's one rule (`sandboxPath`), then
|
|
53
|
+
* by its REAL location, so a symbolic link cannot lead it outside; a file over
|
|
54
|
+
* `maxBytes` is refused, never cut; a file on disk is bytes, so bytes come
|
|
55
|
+
* back and no encoding is guessed. `runCommand(command)` runs ONE `sh -c`
|
|
56
|
+
* command with the same environment allowlist as the code, and exists only on
|
|
57
|
+
* a runner built with `commands: true`. Neither is a network or filesystem
|
|
58
|
+
* jail: a command reaches whatever this machine reaches.
|
|
43
59
|
*
|
|
44
60
|
* So: a development loop, a trusted-input pipeline, a machine you would be
|
|
45
61
|
* relaxed about a shell script running on. NOT arbitrary model-written code
|
|
@@ -64,7 +80,11 @@
|
|
|
64
80
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
65
81
|
exports.localCodeRunner = void 0;
|
|
66
82
|
const types_js_1 = require("../../provider-contract/types.js");
|
|
83
|
+
const codeSandbox_js_1 = require("../../provider-contract/codeSandbox.js");
|
|
84
|
+
const abort_js_1 = require("../../lib/abort.js");
|
|
67
85
|
const lazyRequire_js_1 = require("../../lib/lazyRequire.js");
|
|
86
|
+
const fileName_js_1 = require("./fileName.js");
|
|
87
|
+
const output_js_1 = require("./output.js");
|
|
68
88
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
69
89
|
const DEFAULT_MAX_OUTPUT_CHARS = 8_000;
|
|
70
90
|
/** How each supported language is invoked when no `command` was given. */
|
|
@@ -99,12 +119,30 @@ function localCodeRunner(options = {}) {
|
|
|
99
119
|
let stagingDir;
|
|
100
120
|
/** name → path, exactly as the manifest carries it. */
|
|
101
121
|
const staged = new Map();
|
|
102
|
-
|
|
122
|
+
/** The child's environment, for code and commands alike. */
|
|
123
|
+
const childEnv = () => ({
|
|
124
|
+
// ALLOWLIST. `process.env` is deliberately NOT spread in — only
|
|
125
|
+
// PATH, so the OS can find the interpreter, and it is overridable.
|
|
126
|
+
PATH: process.env.PATH ?? '',
|
|
127
|
+
...(options.env ?? {}),
|
|
128
|
+
// The staged-input manifest joins it LAST and is not overridable:
|
|
129
|
+
// it is not configuration, it is a fact about this execution, and
|
|
130
|
+
// an operator `env` that happened to define the same name would
|
|
131
|
+
// otherwise hand the model paths to files that are not there.
|
|
132
|
+
// Absent entirely when nothing was staged.
|
|
133
|
+
...(staged.size > 0 && {
|
|
134
|
+
[types_js_1.STAGED_INPUTS_ENV]: JSON.stringify(Object.fromEntries(staged)),
|
|
135
|
+
}),
|
|
136
|
+
});
|
|
137
|
+
const refuseIfStopped = (toDo) => {
|
|
138
|
+
if (stopped) {
|
|
139
|
+
throw new Error(`localCodeRunner: session ${sessionId} was stopped; open a new one to ${toDo}.`);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
const session = {
|
|
103
143
|
id: sessionId,
|
|
104
144
|
async execute(exec) {
|
|
105
|
-
|
|
106
|
-
throw new Error(`localCodeRunner: session ${sessionId} was stopped; open a new one to run more code.`);
|
|
107
|
-
}
|
|
145
|
+
refuseIfStopped('run more code');
|
|
108
146
|
const language = exec.language ?? req.language ?? 'javascript';
|
|
109
147
|
const command = options.command ?? DEFAULT_COMMANDS[language.toLowerCase()];
|
|
110
148
|
if (!command || command.length === 0) {
|
|
@@ -115,49 +153,36 @@ function localCodeRunner(options = {}) {
|
|
|
115
153
|
const [file, ...args] = command;
|
|
116
154
|
const raw = await runChild(resolveChildProcess(options), file, [...args, exec.code], {
|
|
117
155
|
cwd: options.cwd ?? process.cwd(),
|
|
118
|
-
|
|
119
|
-
// PATH, so the OS can find the interpreter, and it is overridable.
|
|
120
|
-
//
|
|
121
|
-
// The staged-input manifest joins it LAST and is not overridable:
|
|
122
|
-
// it is not configuration, it is a fact about this execution, and
|
|
123
|
-
// an operator `env` that happened to define the same name would
|
|
124
|
-
// otherwise hand the model paths to files that are not there.
|
|
125
|
-
// Absent entirely when nothing was staged.
|
|
126
|
-
env: {
|
|
127
|
-
PATH: process.env.PATH ?? '',
|
|
128
|
-
...(options.env ?? {}),
|
|
129
|
-
...(staged.size > 0 && {
|
|
130
|
-
[types_js_1.STAGED_INPUTS_ENV]: JSON.stringify(Object.fromEntries(staged)),
|
|
131
|
-
}),
|
|
132
|
-
},
|
|
156
|
+
env: childEnv(),
|
|
133
157
|
timeoutMs: exec.timeoutMs ?? timeoutMs,
|
|
134
158
|
...(exec.signal && { signal: exec.signal }),
|
|
159
|
+
}, {
|
|
160
|
+
timedOut: (ms) => new Error(`localCodeRunner: the code did not finish within ${ms}ms and ${STOPPED}. ` +
|
|
161
|
+
'Raise `timeoutMs`, or have the code do less per call.'),
|
|
162
|
+
notRunnable: (code) => new Error(`localCodeRunner: could not run '${file}' (${code}). ` +
|
|
163
|
+
'Install the interpreter, or pass `command` pointing at one.'),
|
|
164
|
+
failedAfterStart: (reason) => erroredAfterStart('the code', reason),
|
|
135
165
|
});
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
...(stderr.cut && { stderr: true }),
|
|
141
|
-
...((stdout.cut || stderr.cut) && {
|
|
142
|
-
ofChars: Math.max(raw.stdout.length, raw.stderr.length),
|
|
143
|
-
}),
|
|
144
|
-
};
|
|
166
|
+
// The child's own streams only. The runtime's message for a failed
|
|
167
|
+
// child ("Command failed: <the command line>") quotes the code, so
|
|
168
|
+
// it is not passed off as the code's stderr.
|
|
169
|
+
const bounded = (0, output_js_1.boundOutput)(raw.stdout, raw.stderr, maxOutputChars, raw.overflowed);
|
|
145
170
|
return {
|
|
171
|
+
// Known either way: an exit status, or a process that was ended
|
|
172
|
+
// before it exited (no exit status at all — never a made-up one;
|
|
173
|
+
// the signal that ended it, when known, says how).
|
|
146
174
|
ok: raw.exitCode === 0,
|
|
147
|
-
stdout: stdout
|
|
148
|
-
stderr: stderr
|
|
175
|
+
stdout: bounded.stdout,
|
|
176
|
+
stderr: bounded.stderr,
|
|
149
177
|
...(raw.exitCode !== undefined && { exitCode: raw.exitCode }),
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
...(Object.keys(truncated).length > 0 && { truncated }),
|
|
178
|
+
...(raw.signal !== undefined && { terminatedBy: raw.signal }),
|
|
179
|
+
...(bounded.truncated !== undefined && { truncated: bounded.truncated }),
|
|
153
180
|
};
|
|
154
181
|
},
|
|
155
182
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
156
183
|
async stageInputs(inputs) {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
const fs = resolveFs(options);
|
|
184
|
+
refuseIfStopped('stage inputs');
|
|
185
|
+
const fs = resolveFs(options, 'staging inputs');
|
|
161
186
|
const landed = [];
|
|
162
187
|
stagingDir ??= makeStagingDir(fs, options._stagingRoot);
|
|
163
188
|
for (const input of inputs) {
|
|
@@ -166,17 +191,50 @@ function localCodeRunner(options = {}) {
|
|
|
166
191
|
// name of '../../etc/passwd' becomes a literal file name, two
|
|
167
192
|
// distinct names never become one file, and the manifest still
|
|
168
193
|
// keys it by what was asked for.
|
|
169
|
-
const fileName = safeFileName(input.fileName ?? input.name);
|
|
194
|
+
const fileName = (0, fileName_js_1.safeFileName)(input.fileName ?? input.name);
|
|
170
195
|
const path = `${stagingDir}/${fileName}`;
|
|
171
|
-
const bytes =
|
|
172
|
-
? Buffer.byteLength(input.data, 'utf8')
|
|
173
|
-
: input.data.byteLength;
|
|
196
|
+
const bytes = (0, output_js_1.payloadBytes)(input.data);
|
|
174
197
|
writeFile(fs, path, input.data);
|
|
175
198
|
staged.set(input.name, path);
|
|
176
199
|
landed.push({ name: input.name, path, bytes });
|
|
177
200
|
}
|
|
178
201
|
return landed;
|
|
179
202
|
},
|
|
203
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
204
|
+
async readFile(path, readOptions) {
|
|
205
|
+
refuseIfStopped('read files');
|
|
206
|
+
const maxBytes = (0, codeSandbox_js_1.readBound)(readOptions);
|
|
207
|
+
(0, abort_js_1.throwIfAborted)(readOptions.signal);
|
|
208
|
+
const relative = (0, codeSandbox_js_1.sandboxPath)(path);
|
|
209
|
+
return readWithin(resolveFs(options, 'reading files'), options.cwd ?? process.cwd(), relative, maxBytes);
|
|
210
|
+
},
|
|
211
|
+
...(options.commands === true && {
|
|
212
|
+
async runCommand(command, commandOptions) {
|
|
213
|
+
refuseIfStopped('run commands');
|
|
214
|
+
const bounds = (0, output_js_1.commandBounds)(command, commandOptions, maxOutputChars);
|
|
215
|
+
const raw = await runChild(resolveChildProcess(options), 'sh', ['-c', command], {
|
|
216
|
+
cwd: options.cwd ?? process.cwd(),
|
|
217
|
+
env: childEnv(),
|
|
218
|
+
timeoutMs: bounds.timeoutMs ?? timeoutMs,
|
|
219
|
+
...(bounds.signal && { signal: bounds.signal }),
|
|
220
|
+
}, {
|
|
221
|
+
timedOut: (ms) => new codeSandbox_js_1.CodeSandboxError('timed-out', `localCodeRunner: the command did not finish within ${ms}ms and ${STOPPED}.`),
|
|
222
|
+
notRunnable: (code) => new Error(`localCodeRunner: could not run 'sh' (${code}); runCommand needs a POSIX ` +
|
|
223
|
+
'shell on PATH.'),
|
|
224
|
+
failedAfterStart: (reason) => erroredAfterStart('the command', reason),
|
|
225
|
+
});
|
|
226
|
+
// The child's own streams and exit status, nothing added: a
|
|
227
|
+
// process the operating system ended has no exit status, and the
|
|
228
|
+
// result says so by leaving it out.
|
|
229
|
+
return (0, output_js_1.commandResult)({
|
|
230
|
+
stdout: raw.stdout,
|
|
231
|
+
stderr: raw.stderr,
|
|
232
|
+
...(raw.exitCode !== undefined && { exitCode: raw.exitCode }),
|
|
233
|
+
...(raw.overflowed !== undefined && { overflowed: raw.overflowed }),
|
|
234
|
+
...(raw.signal !== undefined && { terminatedBy: raw.signal }),
|
|
235
|
+
}, bounds);
|
|
236
|
+
},
|
|
237
|
+
}),
|
|
180
238
|
stop() {
|
|
181
239
|
// Idempotent, and it refuses later executions rather than silently
|
|
182
240
|
// starting a fresh process under a session the caller closed.
|
|
@@ -187,7 +245,10 @@ function localCodeRunner(options = {}) {
|
|
|
187
245
|
// teardown would fail a turn that already succeeded.
|
|
188
246
|
if (stagingDir !== undefined) {
|
|
189
247
|
try {
|
|
190
|
-
resolveFs(options).rmSync?.(stagingDir, {
|
|
248
|
+
resolveFs(options, 'staging inputs').rmSync?.(stagingDir, {
|
|
249
|
+
recursive: true,
|
|
250
|
+
force: true,
|
|
251
|
+
});
|
|
191
252
|
}
|
|
192
253
|
catch {
|
|
193
254
|
// See above.
|
|
@@ -197,24 +258,29 @@ function localCodeRunner(options = {}) {
|
|
|
197
258
|
}
|
|
198
259
|
return Promise.resolve();
|
|
199
260
|
},
|
|
200
|
-
}
|
|
261
|
+
};
|
|
262
|
+
return Promise.resolve(session);
|
|
201
263
|
},
|
|
202
264
|
};
|
|
203
265
|
}
|
|
204
266
|
exports.localCodeRunner = localCodeRunner;
|
|
205
|
-
/** `node:fs`, or a refusal naming what
|
|
206
|
-
* `node:child_process` is: a browser bundle that never stages
|
|
207
|
-
* for a module it cannot have. */
|
|
208
|
-
function resolveFs(options) {
|
|
267
|
+
/** `node:fs`, or a refusal naming what `purpose` needs. Lazy for the same
|
|
268
|
+
* reason `node:child_process` is: a browser bundle that never stages or
|
|
269
|
+
* reads must not pay for a module it cannot have. */
|
|
270
|
+
function resolveFs(options, purpose) {
|
|
209
271
|
if (options._fs)
|
|
210
272
|
return options._fs;
|
|
211
273
|
try {
|
|
212
274
|
return (0, lazyRequire_js_1.lazyRequire)('node:fs');
|
|
213
275
|
}
|
|
214
276
|
catch {
|
|
215
|
-
throw new Error('
|
|
216
|
-
'
|
|
217
|
-
|
|
277
|
+
throw new Error(purpose === 'staging inputs'
|
|
278
|
+
? 'localCodeRunner: staging inputs needs `node:fs`, which this runtime does not provide ' +
|
|
279
|
+
'(a browser bundle, or a restricted worker). Use a runner whose backend can write ' +
|
|
280
|
+
'into its own session, or drop the tool’s `wants` declaration.'
|
|
281
|
+
: 'localCodeRunner: reading files needs `node:fs`, which this runtime does not provide ' +
|
|
282
|
+
'(a browser bundle, or a restricted worker); a runner whose backend reads its own ' +
|
|
283
|
+
'session can.');
|
|
218
284
|
}
|
|
219
285
|
}
|
|
220
286
|
/** One private directory per session, under the OS temp root. */
|
|
@@ -233,100 +299,120 @@ function writeFile(fs, path, data) {
|
|
|
233
299
|
}
|
|
234
300
|
writeFileSync(path, data);
|
|
235
301
|
}
|
|
236
|
-
/**
|
|
237
|
-
|
|
238
|
-
const FILE_NAME_MAX = 120;
|
|
239
|
-
/** Marks a name this adapter had to rewrite. Nothing arm A returns may start
|
|
240
|
-
* with it. */
|
|
241
|
-
const ENCODED_FILE_PREFIX = '_enc_';
|
|
242
|
-
/** Already one inert segment: no separator, no escape-looking bytes. */
|
|
243
|
-
const FILE_NAME_ALREADY_LEGAL = /^[A-Za-z0-9._-]+$/;
|
|
244
|
-
/** `.`, `..`, `...` — a name that is nothing but dots. Not a hop once the
|
|
245
|
-
* separators are gone, but not a name either, so it goes to arm B. */
|
|
246
|
-
const DOTS_ONLY = /^\.+$/;
|
|
247
|
-
/** Legal AND not the escape character — the set {@link encodeFileName} passes
|
|
248
|
-
* through untouched. */
|
|
249
|
-
const FILE_CHAR_PASSES_THROUGH = /[A-Za-z0-9.-]/;
|
|
302
|
+
/** How much of a file one `readSync` asks for. */
|
|
303
|
+
const READ_CHUNK_BYTES = 64 * 1024;
|
|
250
304
|
/**
|
|
251
|
-
*
|
|
252
|
-
* folding two names into one file**.
|
|
253
|
-
*
|
|
254
|
-
* Separators cannot survive as a parent hop and `a/b` cannot become two
|
|
255
|
-
* segments — the same law `artifacts/scopePath` states for scope tuples,
|
|
256
|
-
* applied to the one other place caller strings become file names in this
|
|
257
|
-
* package. But the old version got there by REPLACING every illegal character
|
|
258
|
-
* with `_`, which meant `a/b.csv` and `a_b.csv` became the same file: the
|
|
259
|
-
* second write clobbered the first's bytes, `stageInputs` answered two manifest
|
|
260
|
-
* entries pointing at one path, and the model's code opened `a/b.csv` and read
|
|
261
|
-
* whatever `a_b.csv` contained. Nothing failed. The computation was simply run
|
|
262
|
-
* against the wrong dataset — which is the worst shape a data bug can take,
|
|
263
|
-
* because the answer still looks like an answer. `.slice(0, 120)` folded the
|
|
264
|
-
* same way for any two names agreeing on their first 120 characters.
|
|
305
|
+
* Read ONE file under `root`, whole or not at all.
|
|
265
306
|
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
307
|
+
* `relative` has already passed the port's lexical rule; this adds the one
|
|
308
|
+
* check only the caller's own machine can make — the file's REAL location,
|
|
309
|
+
* after every symbolic link, must still be inside the real working directory.
|
|
310
|
+
* (A sandbox has no such check to make: its whole disk is its own.) The check
|
|
311
|
+
* and the open are two calls, not one atomic step; this is a development-loop
|
|
312
|
+
* runner, and its module header says what it is not.
|
|
268
313
|
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
* name `codeRunnerTool` derives from a declared argument, all land at
|
|
273
|
-
* exactly the path they landed at before.
|
|
274
|
-
* B. anything else → `_enc_` + an escaped form, where `_` is the escape
|
|
275
|
-
* character (`_` → `__`, any other illegal UTF-16 unit → `_uXXXX`). A
|
|
276
|
-
* decoder is a left inverse of that, which is what makes it injective.
|
|
277
|
-
*
|
|
278
|
-
* Arm A never starts with `_enc_` (excluded by construction) and every arm B
|
|
279
|
-
* output does, so no name from one arm can collide with a name from the other.
|
|
280
|
-
* Both arms produce ASCII with no `/`, no `\` and no NUL, so the path-hop law
|
|
281
|
-
* the old version enforced is unchanged.
|
|
314
|
+
* Bounded twice: the size the file reports is checked before a byte is read,
|
|
315
|
+
* and the read itself stops one chunk past `maxBytes` — a file that grew in
|
|
316
|
+
* between is refused too, never handed back cut.
|
|
282
317
|
*/
|
|
283
|
-
function
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
318
|
+
function readWithin(fs, root, relative, maxBytes) {
|
|
319
|
+
const realpath = fsMember(fs, 'realpathSync');
|
|
320
|
+
const open = fsMember(fs, 'openSync');
|
|
321
|
+
const fstat = fsMember(fs, 'fstatSync');
|
|
322
|
+
const read = fsMember(fs, 'readSync');
|
|
323
|
+
const close = fsMember(fs, 'closeSync');
|
|
324
|
+
let real;
|
|
325
|
+
let realRoot;
|
|
326
|
+
try {
|
|
327
|
+
realRoot = realpath(root);
|
|
328
|
+
real = realpath(`${root}/${relative}`);
|
|
289
329
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
if (
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
330
|
+
catch (err) {
|
|
331
|
+
throw notReturned(relative, errnoOf(err));
|
|
332
|
+
}
|
|
333
|
+
if (!isBelow(realRoot, real)) {
|
|
334
|
+
throw new codeSandbox_js_1.CodeSandboxError('path-refused', `localCodeRunner: '${relative}' leads outside the working directory through a link, ` +
|
|
335
|
+
'so it is not read.', { path: relative });
|
|
336
|
+
}
|
|
337
|
+
let fd;
|
|
338
|
+
try {
|
|
339
|
+
fd = open(real, readFlags(fs));
|
|
340
|
+
}
|
|
341
|
+
catch (err) {
|
|
342
|
+
throw notReturned(relative, errnoOf(err));
|
|
343
|
+
}
|
|
344
|
+
try {
|
|
345
|
+
const stat = fstat(fd);
|
|
346
|
+
if (!stat.isFile())
|
|
347
|
+
throw notReturned(relative, 'not a regular file');
|
|
348
|
+
if (stat.size > maxBytes)
|
|
349
|
+
throw tooLarge(relative, stat.size, maxBytes);
|
|
350
|
+
const chunks = [];
|
|
351
|
+
const chunk = new Uint8Array(Math.min(READ_CHUNK_BYTES, maxBytes + 1));
|
|
352
|
+
let total = 0;
|
|
353
|
+
for (;;) {
|
|
354
|
+
const n = read(fd, chunk, 0, chunk.length, null);
|
|
355
|
+
if (n === 0)
|
|
356
|
+
break;
|
|
357
|
+
total += n;
|
|
358
|
+
if (total > maxBytes)
|
|
359
|
+
throw tooLarge(relative, total, maxBytes);
|
|
360
|
+
chunks.push(chunk.slice(0, n));
|
|
361
|
+
}
|
|
362
|
+
const data = new Uint8Array(total);
|
|
363
|
+
let at = 0;
|
|
364
|
+
for (const part of chunks) {
|
|
365
|
+
data.set(part, at);
|
|
366
|
+
at += part.byteLength;
|
|
367
|
+
}
|
|
368
|
+
return { path: relative, data, bytes: total };
|
|
369
|
+
}
|
|
370
|
+
finally {
|
|
371
|
+
close(fd);
|
|
315
372
|
}
|
|
316
|
-
return out;
|
|
317
373
|
}
|
|
318
374
|
/**
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
|
|
325
|
-
|
|
375
|
+
* How a read opens its file: read-only, and — where the platform has the flags
|
|
376
|
+
* — NON-BLOCKING, so a named pipe the session's code made cannot freeze this
|
|
377
|
+
* process waiting for a writer (the `isFile` check after the open refuses it),
|
|
378
|
+
* and NO-FOLLOW, so a link swapped in after the real-path check is not
|
|
379
|
+
* followed. Plain `'r'` where the platform has neither.
|
|
380
|
+
*/
|
|
381
|
+
function readFlags(fs) {
|
|
382
|
+
const c = fs.constants;
|
|
383
|
+
if (c?.O_NONBLOCK === undefined)
|
|
384
|
+
return 'r';
|
|
385
|
+
return (c.O_RDONLY ?? 0) | c.O_NONBLOCK | (c.O_NOFOLLOW ?? 0);
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Is `candidate` strictly below `root`? Both are real paths from the same
|
|
389
|
+
* `realpathSync`, so they share one spelling of the root and one separator
|
|
390
|
+
* style. String work rather than `node:path`, so reading files adds no Node
|
|
391
|
+
* module to the browser-safe door's call-time graph.
|
|
326
392
|
*/
|
|
327
|
-
function
|
|
328
|
-
const
|
|
329
|
-
|
|
393
|
+
function isBelow(root, candidate) {
|
|
394
|
+
const separator = root.includes('\\') && !root.includes('/') ? '\\' : '/';
|
|
395
|
+
const base = root.endsWith(separator) ? root : `${root}${separator}`;
|
|
396
|
+
return candidate.startsWith(base) && candidate.length > base.length;
|
|
397
|
+
}
|
|
398
|
+
function fsMember(fs, name) {
|
|
399
|
+
const member = fs[name];
|
|
400
|
+
if (typeof member !== 'function') {
|
|
401
|
+
throw new Error(`localCodeRunner: \`node:fs\` resolved but \`${name}\` is missing.`);
|
|
402
|
+
}
|
|
403
|
+
return member;
|
|
404
|
+
}
|
|
405
|
+
function errnoOf(err) {
|
|
406
|
+
const code = err?.code;
|
|
407
|
+
return typeof code === 'string' ? code : 'unreadable';
|
|
408
|
+
}
|
|
409
|
+
function notReturned(relative, why) {
|
|
410
|
+
return new codeSandbox_js_1.CodeSandboxError('file-not-returned', `localCodeRunner: no file could be read at '${relative}' (${why}).`, { path: relative });
|
|
411
|
+
}
|
|
412
|
+
function tooLarge(relative, bytes, maxBytes) {
|
|
413
|
+
return new codeSandbox_js_1.CodeSandboxError('file-too-large', `localCodeRunner: '${relative}' is at least ${bytes} bytes and this read allows ` +
|
|
414
|
+
`${maxBytes}. Nothing was returned — a file is never handed back cut. Raise \`maxBytes\`, ` +
|
|
415
|
+
'or have the code write something smaller.', { path: relative, bytes, limit: maxBytes });
|
|
330
416
|
}
|
|
331
417
|
function resolveChildProcess(options) {
|
|
332
418
|
if (options._childProcess)
|
|
@@ -340,60 +426,320 @@ function resolveChildProcess(options) {
|
|
|
340
426
|
'`agentCoreCodeRunner` — or supply your own `CodeRunner`.');
|
|
341
427
|
}
|
|
342
428
|
}
|
|
429
|
+
/**
|
|
430
|
+
* The answer for a process that started and then had an error reported for
|
|
431
|
+
* it: stopped, its outcome unknown. Never "could not run" — that says nothing
|
|
432
|
+
* ran, and this one may have done some or all of its work. The reason is the
|
|
433
|
+
* runtime's error code, a name; never its message, which can quote the
|
|
434
|
+
* command line.
|
|
435
|
+
*/
|
|
436
|
+
function erroredAfterStart(what, reason) {
|
|
437
|
+
return new codeSandbox_js_1.CodeSandboxError('error-after-start', `localCodeRunner: ${what} started, then the runtime reported an error for its process ` +
|
|
438
|
+
`(${reason}), and ${STOPPED}. Its outcome is unknown: it may have done some or all of ` +
|
|
439
|
+
'its work.');
|
|
440
|
+
}
|
|
441
|
+
/** How much one child's stdout or stderr may hold before it is ended. The CUT
|
|
442
|
+
* is the adapter's to make and report (`maxOutputChars`); this only bounds
|
|
443
|
+
* memory. */
|
|
444
|
+
const CHILD_BUFFER_BYTES = 32 * 1024 * 1024;
|
|
445
|
+
/** The runtime's code for an output that passed `maxBuffer`. */
|
|
446
|
+
const BUFFER_OVERFLOW = 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER';
|
|
447
|
+
/**
|
|
448
|
+
* Whether a child gets a process group of its own, so that the runner can stop
|
|
449
|
+
* the group it owns. POSIX only: Windows has no process group to signal, and
|
|
450
|
+
* there only the child itself can be stopped.
|
|
451
|
+
*/
|
|
452
|
+
const OWN_PROCESS_GROUP = process.platform !== 'win32';
|
|
453
|
+
/** What ending a child at its deadline did, in words that are true of it. */
|
|
454
|
+
const STOPPED = OWN_PROCESS_GROUP
|
|
455
|
+
? 'its process group was killed'
|
|
456
|
+
: 'its process was stopped; work it started may still be running';
|
|
457
|
+
/**
|
|
458
|
+
* Process groups this runner started and has not seen end. A group lives in
|
|
459
|
+
* its own session, so a signal sent to the host's group — Ctrl-C in a
|
|
460
|
+
* terminal — never reaches it. These are killed when the host exits.
|
|
461
|
+
*/
|
|
462
|
+
const liveGroups = new Set();
|
|
463
|
+
let exitHookArmed = false;
|
|
464
|
+
function trackGroup(pid) {
|
|
465
|
+
liveGroups.add(pid);
|
|
466
|
+
if (exitHookArmed)
|
|
467
|
+
return;
|
|
468
|
+
exitHookArmed = true;
|
|
469
|
+
process.once('exit', () => {
|
|
470
|
+
for (const group of liveGroups)
|
|
471
|
+
killGroup(group);
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
function killGroup(pid) {
|
|
475
|
+
try {
|
|
476
|
+
process.kill(-pid, 'SIGKILL');
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
// Already gone: nothing left to stop.
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Stop a child's OWNED PROCESS GROUP, then release its pipes — every path on
|
|
484
|
+
* which the runner ends a run goes through here: the deadline, an abort, the
|
|
485
|
+
* output ceiling, an error after a start.
|
|
486
|
+
*
|
|
487
|
+
* Signalling only the direct child (the shell, or the interpreter) leaves its
|
|
488
|
+
* own children running: a `sh -c` command that started a Node script would
|
|
489
|
+
* keep writing files after the caller was told it had been killed. The group
|
|
490
|
+
* goes, with SIGKILL, which none of its members can ignore, and the pipes are
|
|
491
|
+
* closed so that nothing still holding them keeps the caller waiting.
|
|
492
|
+
*
|
|
493
|
+
* Cleanup, not containment: a descendant that starts a session or a group of
|
|
494
|
+
* its own has left the owned group and is not stopped, and a host that is
|
|
495
|
+
* killed without running its JavaScript exit handlers cannot stop anything.
|
|
496
|
+
* This runner is not a sandbox (see the module header).
|
|
497
|
+
*/
|
|
498
|
+
function endChild(child) {
|
|
499
|
+
if (child === undefined)
|
|
500
|
+
return;
|
|
501
|
+
if (OWN_PROCESS_GROUP && typeof child.pid === 'number') {
|
|
502
|
+
killGroup(child.pid);
|
|
503
|
+
liveGroups.delete(child.pid);
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
try {
|
|
507
|
+
child.kill?.('SIGKILL');
|
|
508
|
+
}
|
|
509
|
+
catch {
|
|
510
|
+
// Already gone.
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
try {
|
|
514
|
+
child.stdout?.destroy?.();
|
|
515
|
+
child.stderr?.destroy?.();
|
|
516
|
+
}
|
|
517
|
+
catch {
|
|
518
|
+
// A stream the runtime already closed.
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* A child `error` EVENT as the callback is handed it: its string code when it
|
|
523
|
+
* has one, and nothing else. Never a numeric code or a signal — those are what
|
|
524
|
+
* an EXIT reports (`childFailure`), so the two cannot be taken for each other —
|
|
525
|
+
* and never the runtime's own error, which carries the arguments
|
|
526
|
+
* (`spawnargs`), and so the code itself.
|
|
527
|
+
*/
|
|
528
|
+
function childEventError(error) {
|
|
529
|
+
return Object.assign(new Error('the runtime reported an error for the child process'), {
|
|
530
|
+
...(typeof error.code === 'string' && { code: error.code }),
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
/** The error a child that exited non-zero, or was ended by a signal, reports:
|
|
534
|
+
* the fields `execFile` carries, and a message that never quotes the
|
|
535
|
+
* command line (the runtime's own does — it is the code). */
|
|
536
|
+
function childFailure(code, signal) {
|
|
537
|
+
return Object.assign(new Error('the child process did not exit cleanly'), {
|
|
538
|
+
...(code !== null && { code }),
|
|
539
|
+
signal,
|
|
540
|
+
killed: signal !== null,
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* `execFile`'s callback contract, built on `spawn` — because only `spawn`
|
|
545
|
+
* passes `detached` on, and a child in a process group of its own is what
|
|
546
|
+
* lets the runner stop the group it owns. Same results: stdout and stderr as
|
|
547
|
+
* text, an exit failure carrying the exit `code` or the `signal`, a child
|
|
548
|
+
* `error` event carrying its string code at most (`childEventError`), and
|
|
549
|
+
* `ERR_CHILD_PROCESS_STDIO_MAXBUFFER` when a stream passes `maxBuffer` bytes,
|
|
550
|
+
* with what was collected up to the ceiling. At that ceiling, and on any child
|
|
551
|
+
* `error` event, the owned group is stopped AT ONCE and the call answered then
|
|
552
|
+
* (`endChild`), not when the pipes close. stdin is not connected: nothing can
|
|
553
|
+
* block waiting for input that never comes.
|
|
554
|
+
*/
|
|
555
|
+
function execFileOn(spawn) {
|
|
556
|
+
return (file, args, options, callback) => {
|
|
557
|
+
const max = options.maxBuffer ?? CHILD_BUFFER_BYTES;
|
|
558
|
+
const chunks = { stdout: [], stderr: [] };
|
|
559
|
+
const sizes = { stdout: 0, stderr: 0 };
|
|
560
|
+
let overflow;
|
|
561
|
+
let done = false;
|
|
562
|
+
const text = (stream) => Buffer.concat(chunks[stream]).toString('utf8');
|
|
563
|
+
const reply = (error) => {
|
|
564
|
+
if (done)
|
|
565
|
+
return;
|
|
566
|
+
done = true;
|
|
567
|
+
callback(error, text('stdout'), text('stderr'));
|
|
568
|
+
};
|
|
569
|
+
const child = spawn(file, args, {
|
|
570
|
+
...(options.cwd !== undefined && { cwd: options.cwd }),
|
|
571
|
+
...(options.env !== undefined && { env: options.env }),
|
|
572
|
+
...(options.detached === true && { detached: true }),
|
|
573
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
574
|
+
});
|
|
575
|
+
const collect = (stream) => (chunk) => {
|
|
576
|
+
if (overflow !== undefined)
|
|
577
|
+
return;
|
|
578
|
+
const room = max - sizes[stream];
|
|
579
|
+
if (chunk.byteLength > room) {
|
|
580
|
+
chunks[stream].push(chunk.subarray(0, Math.max(room, 0)));
|
|
581
|
+
sizes[stream] = max;
|
|
582
|
+
overflow = Object.assign(new RangeError(`${stream} maxBuffer length exceeded`), {
|
|
583
|
+
code: BUFFER_OVERFLOW,
|
|
584
|
+
});
|
|
585
|
+
// Stop the owned process GROUP here, and release the pipes, then
|
|
586
|
+
// answer: waiting for `close` would wait for any descendant that
|
|
587
|
+
// inherited the pipes — and it keeps working until it lets go.
|
|
588
|
+
endChild(child);
|
|
589
|
+
reply(overflow);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
chunks[stream].push(chunk);
|
|
593
|
+
sizes[stream] += chunk.byteLength;
|
|
594
|
+
};
|
|
595
|
+
child.stdout?.on('data', collect('stdout'));
|
|
596
|
+
child.stderr?.on('data', collect('stderr'));
|
|
597
|
+
// A child `error` EVENT is not an exit, and is handed on apart from one.
|
|
598
|
+
// Whatever it carries — a code or none — the runner stops what it owns
|
|
599
|
+
// NOW: a child that started may still be running, and nothing more will be
|
|
600
|
+
// heard from it here. (Before a start there is no group; this is a no-op.)
|
|
601
|
+
child.on('error', (error) => {
|
|
602
|
+
endChild(child);
|
|
603
|
+
reply(childEventError(error));
|
|
604
|
+
});
|
|
605
|
+
child.on('close', (code, signal) => {
|
|
606
|
+
if (overflow !== undefined)
|
|
607
|
+
reply(overflow);
|
|
608
|
+
else if (code === 0 && signal === null)
|
|
609
|
+
reply(null);
|
|
610
|
+
else
|
|
611
|
+
reply(childFailure(code, signal));
|
|
612
|
+
});
|
|
613
|
+
return child;
|
|
614
|
+
};
|
|
615
|
+
}
|
|
343
616
|
/**
|
|
344
617
|
* One child process, promisified.
|
|
345
618
|
*
|
|
346
619
|
* A NON-ZERO EXIT IS NOT A REJECTION. Code that fails is a normal answer here —
|
|
347
620
|
* the model wrote it, it needs to read the traceback, and turning that into a
|
|
348
621
|
* thrown tool error would replace the one thing that teaches the next attempt
|
|
349
|
-
* with `error: true`. What DOES reject is the runner failing to run at all
|
|
350
|
-
* (interpreter missing, timeout, abort),
|
|
622
|
+
* with `error: true`. What DOES reject is the runner failing to run it at all
|
|
623
|
+
* (interpreter missing), stopping it (timeout, abort), or losing it — the
|
|
624
|
+
* runtime reported an error for a process that had started, which is stopped
|
|
625
|
+
* and whose outcome is unknown — because none of those is the code's answer.
|
|
626
|
+
*
|
|
627
|
+
* THE DEADLINE IS OURS (`lib/abort.ts`), not the runtime's `timeout`: that one
|
|
628
|
+
* sends SIGTERM and then waits for the child to exit, so a child that ignored
|
|
629
|
+
* SIGTERM ran on, and if it then exited 0 the call came back as a SUCCESS with
|
|
630
|
+
* its output cut at the deadline. Here the deadline and the caller's signal
|
|
631
|
+
* abort one controller; the child's owned PROCESS GROUP is killed with
|
|
632
|
+
* SIGKILL (`endChild`), and the call rejects at once, whatever the child does
|
|
633
|
+
* next.
|
|
634
|
+
*
|
|
635
|
+
* An abort rejects with the caller's own reason, never read as a missing
|
|
636
|
+
* interpreter. So did an output past the buffer ceiling
|
|
637
|
+
* (`ERR_CHILD_PROCESS_STDIO_MAXBUFFER`); that is now a result, its cut stated,
|
|
638
|
+
* with no exit status and `signal: 'SIGKILL'`.
|
|
639
|
+
*
|
|
640
|
+
* Nothing of the runtime's own error text is passed on: for a failed child it
|
|
641
|
+
* is "Command failed: <the whole command line>", which is the code itself.
|
|
351
642
|
*/
|
|
352
|
-
async function runChild(mod, file, args, opts) {
|
|
353
|
-
const execFile = mod.execFile;
|
|
643
|
+
async function runChild(mod, file, args, opts, refusals) {
|
|
644
|
+
const execFile = typeof mod.spawn === 'function' ? execFileOn(mod.spawn.bind(mod)) : mod.execFile;
|
|
354
645
|
if (typeof execFile !== 'function') {
|
|
355
646
|
throw new Error('localCodeRunner: `node:child_process` resolved but `execFile` is missing.');
|
|
356
647
|
}
|
|
648
|
+
(0, abort_js_1.throwIfAborted)(opts.signal);
|
|
649
|
+
const { controller, unlink } = (0, abort_js_1.linkedController)(opts.signal);
|
|
650
|
+
const disarm = (0, abort_js_1.armDeadline)(controller, opts.timeoutMs, () => refusals.timedOut(opts.timeoutMs));
|
|
357
651
|
return new Promise((resolve, reject) => {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
652
|
+
let child;
|
|
653
|
+
let settled = false;
|
|
654
|
+
const settle = () => {
|
|
655
|
+
if (settled)
|
|
656
|
+
return false;
|
|
657
|
+
settled = true;
|
|
658
|
+
disarm();
|
|
659
|
+
unlink();
|
|
660
|
+
controller.signal.removeEventListener('abort', onAbort);
|
|
661
|
+
if (OWN_PROCESS_GROUP && typeof child?.pid === 'number')
|
|
662
|
+
liveGroups.delete(child.pid);
|
|
663
|
+
return true;
|
|
664
|
+
};
|
|
665
|
+
// The deadline, or the caller: stop the owned group and answer NOW. The
|
|
666
|
+
// child's own callback, whenever it comes, finds the call settled.
|
|
667
|
+
const onAbort = () => {
|
|
668
|
+
endChild(child);
|
|
669
|
+
if (settle())
|
|
670
|
+
reject((0, abort_js_1.abortErrorOf)(controller.signal));
|
|
671
|
+
};
|
|
672
|
+
try {
|
|
673
|
+
child = execFile(file, args, {
|
|
674
|
+
cwd: opts.cwd,
|
|
675
|
+
env: opts.env,
|
|
676
|
+
maxBuffer: CHILD_BUFFER_BYTES,
|
|
677
|
+
killSignal: 'SIGKILL',
|
|
678
|
+
...(OWN_PROCESS_GROUP && { detached: true }),
|
|
679
|
+
}, (error, stdout, stderr) => {
|
|
680
|
+
if (controller.signal.aborted) {
|
|
681
|
+
onAbort();
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
if (!settle())
|
|
685
|
+
return;
|
|
686
|
+
const out = String(stdout ?? '');
|
|
687
|
+
const err = String(stderr ?? '');
|
|
688
|
+
if (!error) {
|
|
689
|
+
resolve({ stdout: out, stderr: err, exitCode: 0 });
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
if (error.code === BUFFER_OVERFLOW) {
|
|
693
|
+
// On the `spawn` path the group was stopped where the overflow was
|
|
694
|
+
// seen; this covers an injected `execFile`, which ends only the
|
|
695
|
+
// direct child. A second stop of a stopped group is a no-op.
|
|
696
|
+
endChild(child);
|
|
697
|
+
resolve({
|
|
698
|
+
stdout: out,
|
|
699
|
+
stderr: err,
|
|
700
|
+
signal: 'SIGKILL',
|
|
701
|
+
overflowed: /^stderr/.test(error.message) ? 'stderr' : 'stdout',
|
|
702
|
+
});
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
if (typeof error.code === 'number' || typeof error.signal === 'string') {
|
|
706
|
+
// An EXIT — a real non-zero one, or a child the operating system
|
|
707
|
+
// ended: the code ran and failed. That IS the result. The exit
|
|
708
|
+
// status is the child's own, or absent — never a stand-in; the
|
|
709
|
+
// signal says how a child without one was ended.
|
|
710
|
+
resolve({
|
|
711
|
+
stdout: out,
|
|
712
|
+
stderr: err,
|
|
713
|
+
...(typeof error.code === 'number' && { exitCode: error.code }),
|
|
714
|
+
...(typeof error.signal === 'string' && { signal: error.signal }),
|
|
715
|
+
});
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
// Not an exit: the runtime's `error` event, with a code or without.
|
|
719
|
+
// What the runner owns is stopped whatever it carries (a no-op when
|
|
720
|
+
// `execFileOn` already did), and the answer says whether anything
|
|
721
|
+
// ran. A child the runtime gave a process id STARTED, and may have
|
|
722
|
+
// done some or all of its work: its outcome is unknown. One it did
|
|
723
|
+
// not never ran — ENOENT and friends, the interpreter is not here;
|
|
724
|
+
// naming it beats a traceback the model cannot act on.
|
|
725
|
+
endChild(child);
|
|
726
|
+
const reason = typeof error.code === 'string' && error.code !== '' ? error.code : 'no error code';
|
|
727
|
+
reject(typeof child?.pid === 'number'
|
|
728
|
+
? refusals.failedAfterStart(reason)
|
|
729
|
+
: refusals.notRunnable(reason));
|
|
389
730
|
});
|
|
390
|
-
}
|
|
731
|
+
}
|
|
732
|
+
catch (err) {
|
|
733
|
+
if (settle())
|
|
734
|
+
reject(err);
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
if (OWN_PROCESS_GROUP && typeof child?.pid === 'number' && !settled)
|
|
738
|
+
trackGroup(child.pid);
|
|
739
|
+
if (controller.signal.aborted)
|
|
740
|
+
onAbort();
|
|
741
|
+
else if (!settled)
|
|
742
|
+
controller.signal.addEventListener('abort', onAbort, { once: true });
|
|
391
743
|
});
|
|
392
744
|
}
|
|
393
|
-
/** Cut to `max` characters, saying whether it cut. */
|
|
394
|
-
function cut(text, max) {
|
|
395
|
-
if (text.length <= max)
|
|
396
|
-
return { text, cut: false };
|
|
397
|
-
return { text: text.slice(0, max), cut: true };
|
|
398
|
-
}
|
|
399
745
|
//# sourceMappingURL=local.js.map
|