pi-supernova 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -3
- package/docs/CHANGELOG.md +114 -0
- package/docs/TOKEN_COSTS.md +13 -5
- package/index.js +120 -79
- package/package.json +1 -1
- package/src/adapters/bash.js +73 -0
- package/src/adapters/edit.js +249 -0
- package/src/adapters/errors.js +31 -0
- package/src/adapters/index.js +31 -0
- package/src/adapters/list.js +102 -0
- package/src/adapters/read.js +805 -0
- package/src/adapters/refs.js +41 -0
- package/src/adapters/write.js +96 -0
- package/src/bridge/catalog.js +28 -222
- package/src/bridge/host-bridge.js +113 -1668
- package/src/bridge/invoke.js +35 -0
- package/src/bridge/native-tools.js +1 -198
- package/src/context/evidence.js +140 -76
- package/src/context/fuzzy.js +42 -24
- package/src/context/ledger.js +43 -24
- package/src/context/outline.js +23 -18
- package/src/context/repo-index.js +206 -170
- package/src/context/search.js +157 -77
- package/src/context/snap.js +240 -136
- package/src/context/spans.js +2 -1
- package/src/context/surface.js +14 -5
- package/src/contract/bash.js +31 -0
- package/src/contract/edit.js +95 -0
- package/src/contract/read.js +220 -0
- package/src/fs/check.js +12 -8
- package/src/fs/diff.js +18 -7
- package/src/fs/json-read.js +66 -35
- package/src/fs/patch.js +94 -50
- package/src/fs/source-window.js +82 -0
- package/src/fs/text-ops.js +512 -0
- package/src/fs/vfs.js +205 -175
- package/src/fs/workspace.js +119 -108
- package/src/output/bottleneck.js +195 -116
- package/src/output/format.js +101 -67
- package/src/runtime/guest-deny-imports.js +34 -0
- package/src/runtime/guest-worker.js +296 -292
- package/src/runtime/parallel.js +97 -64
- package/src/runtime/program-batch.js +178 -69
- package/src/runtime/reference.js +15 -14
- package/src/runtime/runtime.js +327 -187
- package/src/shared/decode.js +58 -36
- package/src/ui/omp-frame.js +59 -42
- package/src/ui/render-measure.js +51 -29
- package/src/ui/render.js +241 -145
|
@@ -1,8 +1,35 @@
|
|
|
1
1
|
import { parentPort } from "node:worker_threads";
|
|
2
2
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
-
import
|
|
3
|
+
import * as nodeModule from "node:module";
|
|
4
|
+
import { isString, isObject, isFunction, toPlain, looksLikePath } from "../shared/decode.js";
|
|
4
5
|
import { truncateChars } from "../output/format.js";
|
|
5
|
-
import {
|
|
6
|
+
import { gatherReadArgs, normalizeRead, decodeReadValue, assertReadPaths } from "../contract/read.js";
|
|
7
|
+
import { classifyEdit } from "../contract/edit.js";
|
|
8
|
+
import { normalizeBash } from "../contract/bash.js";
|
|
9
|
+
import { guestImportMessage, isDeniedGuestImport } from "./guest-deny-imports.js";
|
|
10
|
+
|
|
11
|
+
const { register, registerHooks } = nodeModule;
|
|
12
|
+
|
|
13
|
+
if (isFunction(registerHooks)) {
|
|
14
|
+
registerHooks({
|
|
15
|
+
resolve(specifier, context, nextResolve) {
|
|
16
|
+
if (isDeniedGuestImport(specifier)) {
|
|
17
|
+
const error = new Error(guestImportMessage(specifier));
|
|
18
|
+
error.code = "ERR_GUEST_IMPORT";
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return nextResolve(specifier, context);
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
} else if (isFunction(register)) {
|
|
26
|
+
try {
|
|
27
|
+
register("./guest-deny-imports.js", import.meta.url);
|
|
28
|
+
} catch {
|
|
29
|
+
// Hosts whose module.register cannot run loader hooks lose the deny list;
|
|
30
|
+
// the guest remains trusted code, not a sandbox boundary.
|
|
31
|
+
}
|
|
32
|
+
}
|
|
6
33
|
|
|
7
34
|
// Guest programs run here, off the host thread. The host can terminate() this
|
|
8
35
|
// worker mid-loop, so a runaway "while (true) {}" or process.exit() in guest
|
|
@@ -14,6 +41,10 @@ const PARAMS = ["console", "read", "edit", "write", "bash"];
|
|
|
14
41
|
|
|
15
42
|
const BODY_LINE_OFFSET = 2;
|
|
16
43
|
|
|
44
|
+
const QUERY_URI = /^(agent|artifact):\/\/.*\?/i;
|
|
45
|
+
|
|
46
|
+
const PER_PATH_HINT = "; use Promise.allSettled(paths.map(path => read(path))) for per-path outcomes";
|
|
47
|
+
|
|
17
48
|
/** Best-effort guest line:col from an error stack (V8 "<anonymous>:L:C", JSC "eval code").*/
|
|
18
49
|
function guestLocation(err) {
|
|
19
50
|
const stack = String(err?.stack);
|
|
@@ -71,16 +102,6 @@ function unwrapRead(res, args) {
|
|
|
71
102
|
return value;
|
|
72
103
|
}
|
|
73
104
|
|
|
74
|
-
function unwrapJsonValue(res) {
|
|
75
|
-
const value = unwrapValue(res);
|
|
76
|
-
|
|
77
|
-
try {
|
|
78
|
-
return JSON.parse(value);
|
|
79
|
-
} catch {
|
|
80
|
-
return value;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
105
|
/** Keep details/truncated reachable but out of the returned literal unless they carry signal. */
|
|
85
106
|
function leanEnvelope(res) {
|
|
86
107
|
if (!isObject(res)) return res;
|
|
@@ -94,338 +115,310 @@ function leanEnvelope(res) {
|
|
|
94
115
|
return res;
|
|
95
116
|
}
|
|
96
117
|
|
|
97
|
-
function
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function buildGuestApi(available, batchRead, runId, _nativeArgv) {
|
|
102
|
-
const rpc = (method, args) => callRpc(runId, method, args);
|
|
103
|
-
const availableSet = new Set(available);
|
|
104
|
-
const checkpointScope = new AsyncLocalStorage();
|
|
105
|
-
let checkpoint = null;
|
|
118
|
+
function swallow(promise) {
|
|
119
|
+
promise.catch(() => {});
|
|
106
120
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (token ? token !== checkpoint : checkpoint !== null) throw new Error("await the active edit checkpoint before issuing other commands; completed checkpoints cannot issue commands");
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
let operationTail = Promise.resolve();
|
|
114
|
-
|
|
115
|
-
const enqueueHost = operation => {
|
|
116
|
-
const next = operationTail.then(operation);
|
|
117
|
-
operationTail = next.then(() => {}, () => {});
|
|
121
|
+
return promise;
|
|
122
|
+
}
|
|
118
123
|
|
|
119
|
-
|
|
120
|
-
};
|
|
124
|
+
function throwReadPathError(target, detail) {
|
|
125
|
+
throw new Error(`read failed for ${target}: ${detail}${PER_PATH_HINT}`);
|
|
126
|
+
}
|
|
121
127
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const promise = enqueueHost(() => rpc("call", [name, args]).then(leanEnvelope));
|
|
128
|
+
function isQueryUri(item) {
|
|
129
|
+
return QUERY_URI.test(item);
|
|
130
|
+
}
|
|
126
131
|
|
|
127
|
-
|
|
132
|
+
function firstItemErrorIndex(res) {
|
|
133
|
+
return res?.itemErrors?.findIndex(error => error != null) ?? -1;
|
|
134
|
+
}
|
|
128
135
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
assertScope(); flushReads();
|
|
133
|
-
const promise = enqueueHost(async () => {
|
|
134
|
-
const wave = await rpc("callMany", [calls]);
|
|
135
|
-
const results = Array.isArray(wave?.results) ? wave.results : Array.isArray(wave) ? wave : [];
|
|
136
|
-
Object.defineProperties(results, {
|
|
137
|
-
mode: { value: wave?.mode, enumerable: false },
|
|
138
|
-
reason: { value: wave?.reason, enumerable: false },
|
|
139
|
-
results: { value: results, enumerable: false },
|
|
140
|
-
});
|
|
136
|
+
function missingResolvedIndex(values, paths) {
|
|
137
|
+
return values.findIndex((value, index) => value?.status === "not_found" && looksLikePath(paths[index]));
|
|
138
|
+
}
|
|
141
139
|
|
|
142
|
-
|
|
143
|
-
|
|
140
|
+
async function rpcReadWave(rpc, wave) {
|
|
141
|
+
if (wave.length === 1) {
|
|
142
|
+
const res = leanEnvelope(await rpc("call", ["read", wave[0].args]));
|
|
144
143
|
|
|
145
|
-
|
|
144
|
+
return { values: [unwrapRead(res, wave[0].args)], errors: [] };
|
|
145
|
+
}
|
|
146
|
+
const args = { ...wave[0].args, path: wave.map(job => job.args.path), _independent: true };
|
|
147
|
+
const res = leanEnvelope(await rpc("call", ["read", args]));
|
|
148
|
+
unwrapRead(res, args);
|
|
146
149
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
async speculate(fn) {
|
|
150
|
-
if (checkpoint) throw new Error("edit checkpoints cannot overlap or nest; await the current checkpoint");
|
|
151
|
-
const token = {};
|
|
152
|
-
checkpoint = token;
|
|
153
|
-
let began = false;
|
|
150
|
+
return { values: res.items, errors: res.itemErrors ?? [] };
|
|
151
|
+
}
|
|
154
152
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
await enqueueHost(() => rpc("speculateBegin", []));
|
|
158
|
-
began = true;
|
|
159
|
-
const value = await checkpointScope.run(token, fn);
|
|
160
|
-
await drainReads();
|
|
161
|
-
await enqueueHost(() => rpc("speculateCommit", []));
|
|
153
|
+
function settleReadWave(wave, values, errors) {
|
|
154
|
+
if (!Array.isArray(values) || values.length !== wave.length) throw new Error("invalid batch read response");
|
|
162
155
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
156
|
+
for (let i = 0; i < wave.length; i++) {
|
|
157
|
+
if (errors[i]) wave[i].reject(new Error(errors[i]));
|
|
158
|
+
else wave[i].resolve(values[i]);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
166
161
|
|
|
167
|
-
|
|
162
|
+
function rejectReadWave(wave, error) {
|
|
163
|
+
for (const job of wave) job.reject(error);
|
|
164
|
+
}
|
|
168
165
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
has: (name) => availableSet.has(name),
|
|
176
|
-
};
|
|
166
|
+
function dispatchReadWaves(pending, pendingReadWaves, enqueueHost, rpc) {
|
|
167
|
+
for (let start = 0; start < pending.length; start += 64) {
|
|
168
|
+
const wave = pending.slice(start, start + 64);
|
|
169
|
+
const delivery = enqueueHost(() => rpcReadWave(rpc, wave))
|
|
170
|
+
.then(({ values, errors }) => settleReadWave(wave, values, errors))
|
|
171
|
+
.catch(error => rejectReadWave(wave, error));
|
|
177
172
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
173
|
+
pendingReadWaves.add(delivery);
|
|
174
|
+
void delivery.finally(() => pendingReadWaves.delete(delivery));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
181
177
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
queuedReads = [];
|
|
178
|
+
function enqueueCompatibleRead(readState, flushReads, args, decode) {
|
|
179
|
+
const key = JSON.stringify({ ...args, path: undefined });
|
|
185
180
|
|
|
186
|
-
|
|
187
|
-
const wave = pending.slice(start, start + 64);
|
|
188
|
-
const args = { ...wave[0].args, path: wave.map(job => job.args.path), _independent: true };
|
|
181
|
+
if (readState.queued.length && readState.queued[0].key !== key) flushReads();
|
|
189
182
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
: rpc("call", ["read", args]).then(leanEnvelope).then(res => { unwrapRead(res, args);
|
|
183
|
+
const promise = new Promise((resolve, reject) => {
|
|
184
|
+
readState.queued.push({ args, key, resolve, reject });
|
|
193
185
|
|
|
194
|
-
|
|
186
|
+
if (readState.queued.length === 1) queueMicrotask(flushReads);
|
|
187
|
+
}).then(decode);
|
|
195
188
|
|
|
196
|
-
|
|
197
|
-
|
|
189
|
+
return swallow(promise);
|
|
190
|
+
}
|
|
198
191
|
|
|
199
|
-
|
|
200
|
-
|
|
192
|
+
async function readManyPaths(readOne, invoke, batchRead, args, paths, decode) {
|
|
193
|
+
assertReadPaths(paths);
|
|
194
|
+
const readEach = async () => {
|
|
195
|
+
const values = await Promise.all(paths.map(item => readOne({ ...args, path: item })));
|
|
196
|
+
const missing = args.resolve ? missingResolvedIndex(values, paths) : -1;
|
|
201
197
|
|
|
202
|
-
|
|
203
|
-
else wave[i].resolve(value);
|
|
204
|
-
}
|
|
205
|
-
}).catch(error => { for (const job of wave) job.reject(error); });
|
|
198
|
+
if (missing >= 0) throwReadPathError(paths[missing], "not_found");
|
|
206
199
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
}
|
|
200
|
+
return values;
|
|
201
|
+
};
|
|
211
202
|
|
|
212
|
-
|
|
213
|
-
for (;;) {
|
|
214
|
-
flushReads();
|
|
215
|
-
const waves = [...pendingReadWaves];
|
|
203
|
+
const guardedReadEach = () => swallow(readEach());
|
|
216
204
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
205
|
+
if (!batchRead || args.resolve || paths.some(isQueryUri)) return guardedReadEach();
|
|
206
|
+
const res = await invoke("read", args);
|
|
207
|
+
const failed = firstItemErrorIndex(res);
|
|
221
208
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const promise = nova.call(name, args);
|
|
209
|
+
if (failed >= 0) throwReadPathError(paths[failed], res.itemErrors[failed]);
|
|
210
|
+
unwrapRead(res, args);
|
|
225
211
|
|
|
226
|
-
|
|
212
|
+
if (Array.isArray(res?.items)) return res.items.map(decode);
|
|
227
213
|
|
|
228
|
-
|
|
229
|
-
|
|
214
|
+
return guardedReadEach();
|
|
215
|
+
}
|
|
230
216
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
217
|
+
function attachCallManyMeta(wave) {
|
|
218
|
+
const results = Array.isArray(wave?.results) ? wave.results : Array.isArray(wave) ? wave : [];
|
|
219
|
+
Object.defineProperties(results, {
|
|
220
|
+
mode: { value: wave?.mode, enumerable: false },
|
|
221
|
+
reason: { value: wave?.reason, enumerable: false },
|
|
222
|
+
results: { value: results, enumerable: false },
|
|
223
|
+
});
|
|
234
224
|
|
|
235
|
-
|
|
225
|
+
return results;
|
|
226
|
+
}
|
|
236
227
|
|
|
237
|
-
|
|
238
|
-
|
|
228
|
+
async function runSpeculation(fn, token, checkpointScope, drainReads, enqueueHost, rpc) {
|
|
229
|
+
let began = false;
|
|
239
230
|
|
|
240
|
-
|
|
241
|
-
|
|
231
|
+
try {
|
|
232
|
+
await drainReads();
|
|
233
|
+
await enqueueHost(() => rpc("speculateBegin", []));
|
|
234
|
+
began = true;
|
|
235
|
+
const value = await checkpointScope.run(token, fn);
|
|
236
|
+
await drainReads();
|
|
237
|
+
await enqueueHost(() => rpc("speculateCommit", []));
|
|
238
|
+
|
|
239
|
+
return { ok: true, committed: true, value };
|
|
240
|
+
} catch (err) {
|
|
241
|
+
await drainReads();
|
|
242
242
|
|
|
243
|
-
|
|
244
|
-
assertScope();
|
|
245
|
-
const args = sessionJsonArgs(readArgs(p, a, b));
|
|
246
|
-
if (isString(args.path) && args.resolve === undefined && args.complete !== true && args.json === undefined && args.about === undefined && args.query === undefined && !looksLikePath(args.path) && !/^(?:agent|artifact):\/\//i.test(args.path)) {
|
|
247
|
-
args.resolve = true;
|
|
248
|
-
}
|
|
249
|
-
validateJsonRead(args);
|
|
243
|
+
if (began) await enqueueHost(() => rpc("speculateRollback", []));
|
|
250
244
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
245
|
+
return { ok: false, committed: false, error: err instanceof Error ? err.message : String(err) };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
254
248
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
const focusModes = [args.about !== undefined, args.query !== undefined, args.outline === true].filter(Boolean).length;
|
|
249
|
+
function formatBashFailure(command, res) {
|
|
250
|
+
let exitCode;
|
|
258
251
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
if ((!args.resolve && args.json === undefined) || !isString(value)) return value;
|
|
252
|
+
try {
|
|
253
|
+
exitCode = JSON.parse(res.details).exitCode;
|
|
254
|
+
} catch {}
|
|
263
255
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
} catch (error) {
|
|
267
|
-
const selector = args.json === undefined ? "" : " (" + (Array.isArray(args.json) ? args.json.join(", ") : String(args.json)) + ")";
|
|
268
|
-
throw new Error("JSON read failed for " + String(args.path ?? args.target ?? "resource") + selector + ": " + (error instanceof Error ? error.message : String(error)));
|
|
269
|
-
}
|
|
270
|
-
};
|
|
256
|
+
const output = String(res.value).trimEnd();
|
|
257
|
+
const suffix = Number.isInteger(exitCode) ? " (exit " + exitCode + ")" : "";
|
|
271
258
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
p = args.path;
|
|
259
|
+
return "command failed" + suffix + ": " + command + (output ? "\n" + output : "");
|
|
260
|
+
}
|
|
275
261
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
return unwrapJsonValue(await invoke("evidence", { ...args, path: evidencePath, query }));
|
|
279
|
-
}
|
|
262
|
+
function markTruncatedOutput(res, text) {
|
|
263
|
+
if (res?.truncated && isString(text) && !text.includes("truncated")) return text + "\n…[output truncated]…";
|
|
280
264
|
|
|
281
|
-
|
|
265
|
+
return text;
|
|
266
|
+
}
|
|
282
267
|
|
|
283
|
-
|
|
284
|
-
|
|
268
|
+
function encodeConsoleArg(a) {
|
|
269
|
+
if (isString(a)) return a;
|
|
285
270
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const missing = args.resolve ? values.findIndex((value, index) => value?.status === "not_found" && looksLikePath(p[index])) : -1;
|
|
271
|
+
try {
|
|
272
|
+
const plain = toPlain(a);
|
|
273
|
+
const encoded = JSON.stringify(plain);
|
|
290
274
|
|
|
291
|
-
|
|
275
|
+
return encoded === undefined ? String(plain) : encoded;
|
|
276
|
+
} catch {
|
|
277
|
+
return String(a);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
292
280
|
|
|
293
|
-
|
|
294
|
-
|
|
281
|
+
function compileGuest(prepared, data) {
|
|
282
|
+
const bindings = data === undefined ? PARAMS : [...PARAMS, "data"];
|
|
295
283
|
|
|
296
|
-
|
|
297
|
-
|
|
284
|
+
return { fn: new AsyncFunction(...bindings, prepared.body), hasReturn: prepared.hasReturn };
|
|
285
|
+
}
|
|
298
286
|
|
|
299
|
-
|
|
287
|
+
function plainGuestValue(value) {
|
|
288
|
+
try { return toPlain(value); }
|
|
289
|
+
catch (err) { return "[unserializable: " + (err?.message || err) + "]"; }
|
|
290
|
+
}
|
|
300
291
|
|
|
301
|
-
|
|
302
|
-
|
|
292
|
+
function handleRpcResult(msg) {
|
|
293
|
+
const pending = pendingRpc.get(msg.id);
|
|
303
294
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const failed = res?.itemErrors?.findIndex(error => error != null) ?? -1;
|
|
295
|
+
if (!pending) return;
|
|
296
|
+
pendingRpc.delete(msg.id);
|
|
307
297
|
|
|
308
|
-
|
|
309
|
-
|
|
298
|
+
if (msg.ok) pending.resolve(msg.value);
|
|
299
|
+
else pending.reject(new Error(msg.error));
|
|
300
|
+
}
|
|
310
301
|
|
|
311
|
-
|
|
302
|
+
function buildGuestApi(available, batchRead, runId, _nativeArgv) {
|
|
303
|
+
const rpc = (method, args) => callRpc(runId, method, args);
|
|
304
|
+
const checkpointScope = new AsyncLocalStorage();
|
|
305
|
+
let checkpoint = null;
|
|
312
306
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}
|
|
307
|
+
const assertScope = () => {
|
|
308
|
+
const token = checkpointScope.getStore();
|
|
316
309
|
|
|
317
|
-
if (
|
|
318
|
-
|
|
319
|
-
unwrapRead(res, args);
|
|
310
|
+
if (token ? token !== checkpoint : checkpoint !== null) throw new Error("await the active edit checkpoint before issuing other commands; completed checkpoints cannot issue commands");
|
|
311
|
+
};
|
|
320
312
|
|
|
321
|
-
|
|
322
|
-
}
|
|
313
|
+
let operationTail = Promise.resolve();
|
|
323
314
|
|
|
324
|
-
|
|
315
|
+
const enqueueHost = operation => {
|
|
316
|
+
const next = operationTail.then(operation);
|
|
317
|
+
operationTail = next.then(() => {}, () => {});
|
|
325
318
|
|
|
326
|
-
|
|
319
|
+
return next;
|
|
320
|
+
};
|
|
327
321
|
|
|
328
|
-
|
|
329
|
-
|
|
322
|
+
const nova = {
|
|
323
|
+
call(name, args) {
|
|
324
|
+
assertScope(); flushReads();
|
|
330
325
|
|
|
331
|
-
|
|
332
|
-
}
|
|
326
|
+
return swallow(enqueueHost(() => rpc("call", [name, args]).then(leanEnvelope)));
|
|
327
|
+
},
|
|
328
|
+
callMany(calls) {
|
|
329
|
+
assertScope(); flushReads();
|
|
333
330
|
|
|
334
|
-
|
|
331
|
+
return swallow(enqueueHost(async () => attachCallManyMeta(await rpc("callMany", [calls]))));
|
|
332
|
+
},
|
|
333
|
+
async speculate(fn) {
|
|
334
|
+
if (checkpoint) throw new Error("edit checkpoints cannot overlap or nest; await the current checkpoint");
|
|
335
|
+
const token = {};
|
|
336
|
+
checkpoint = token;
|
|
335
337
|
|
|
336
|
-
|
|
338
|
+
try { return await runSpeculation(fn, token, checkpointScope, drainReads, enqueueHost, rpc); }
|
|
339
|
+
finally { checkpoint = null; }
|
|
340
|
+
},
|
|
337
341
|
};
|
|
338
342
|
|
|
339
|
-
|
|
343
|
+
// Coalesce already-started compatible reads without rewriting JS control flow.
|
|
344
|
+
const readState = { queued: [], waves: new Set() };
|
|
340
345
|
|
|
341
|
-
|
|
342
|
-
const
|
|
343
|
-
|
|
346
|
+
function flushReads() {
|
|
347
|
+
const pending = readState.queued;
|
|
348
|
+
readState.queued = [];
|
|
349
|
+
dispatchReadWaves(pending, readState.waves, enqueueHost, rpc);
|
|
350
|
+
}
|
|
344
351
|
|
|
345
|
-
|
|
352
|
+
async function drainReads() {
|
|
353
|
+
for (;;) {
|
|
354
|
+
flushReads();
|
|
355
|
+
const waves = [...readState.waves];
|
|
346
356
|
|
|
347
|
-
|
|
348
|
-
|
|
357
|
+
if (!waves.length) return;
|
|
358
|
+
await Promise.allSettled(waves);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
349
361
|
|
|
350
|
-
const
|
|
362
|
+
const invoke = (name, args) => {
|
|
363
|
+
assertScope(); flushReads();
|
|
351
364
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
const usage = 'invalid edit signature; use edit(path,oldText,newText), edit({path,edits:[{oldText,newText}]}), or edit({path,patch:"@@ -1 +1 @@\n-old\n+new\n"})';
|
|
365
|
+
return swallow(nova.call(name, args));
|
|
366
|
+
};
|
|
355
367
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
368
|
+
const read = async (p, a, b) => {
|
|
369
|
+
assertScope();
|
|
370
|
+
const args = normalizeRead(gatherReadArgs(p, a, b));
|
|
371
|
+
const decode = value => decodeReadValue(args, value);
|
|
372
|
+
p = args.path;
|
|
360
373
|
|
|
361
|
-
|
|
374
|
+
if (Array.isArray(p)) {
|
|
375
|
+
const values = await readManyPaths(read, invoke, batchRead, args, p, decode);
|
|
376
|
+
for (const item of p) if (isString(item)) noteReadPath({ path: item }, values);
|
|
362
377
|
|
|
363
|
-
return
|
|
378
|
+
return values;
|
|
364
379
|
}
|
|
365
380
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
381
|
+
const readValue = !batchRead
|
|
382
|
+
? decode(unwrapRead(await invoke("read", args), args))
|
|
383
|
+
: await enqueueCompatibleRead(readState, flushReads, args, decode);
|
|
384
|
+
noteReadPath(args, readValue);
|
|
370
385
|
|
|
371
|
-
|
|
372
|
-
|
|
386
|
+
return readValue;
|
|
387
|
+
};
|
|
373
388
|
|
|
374
|
-
|
|
389
|
+
const readFiles = new Set();
|
|
375
390
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
391
|
+
function noteReadPath(args, value) {
|
|
392
|
+
if (isString(args.path) && looksLikePath(args.path)) readFiles.add(args.path);
|
|
393
|
+
if (isObject(value) && isString(value.path) && looksLikePath(value.path)) readFiles.add(value.path);
|
|
394
|
+
}
|
|
380
395
|
|
|
381
|
-
|
|
396
|
+
const write = async (p, content) => {
|
|
397
|
+
const args = isObject(p) ? p : { path: p, content };
|
|
382
398
|
|
|
383
|
-
|
|
399
|
+
if (args.append !== true && args.replace !== true && isString(args.path) && readFiles.has(args.path)) {
|
|
400
|
+
throw new Error("file was already read this program; use edit(oldText, newText) or edit(view, ...). write({path,content,replace:true}) replaces anyway");
|
|
384
401
|
}
|
|
385
402
|
|
|
386
|
-
return unwrapValue(await invoke(
|
|
403
|
+
return unwrapValue(await invoke("write", args));
|
|
387
404
|
};
|
|
388
405
|
|
|
389
|
-
const
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
if (args.args !== undefined) {
|
|
393
|
-
if (!isString(args.command) || !Array.isArray(args.args)) throw new Error("bash argv requires a command string and an array of string args");
|
|
406
|
+
const edit = async (p, oldText, newText) => {
|
|
407
|
+
const classified = classifyEdit(p, oldText, newText);
|
|
394
408
|
|
|
395
|
-
|
|
396
|
-
args.args = args.args.map(String);
|
|
409
|
+
if (classified.kind === "checkpoint") return nova.speculate(classified.fn);
|
|
397
410
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
if (process.platform === "win32") {
|
|
401
|
-
delete args._directArgv;
|
|
402
|
-
args.command = [args.command, ...args.args].map(quoteShellArg).join(" ");
|
|
403
|
-
delete args.args;
|
|
404
|
-
} else args._directArgv = true;
|
|
405
|
-
}
|
|
411
|
+
return unwrapValue(await invoke(classified.command, classified.args));
|
|
412
|
+
};
|
|
406
413
|
|
|
414
|
+
const bash = async (command, opts) => {
|
|
415
|
+
const args = normalizeBash(command, opts);
|
|
407
416
|
command = args.command;
|
|
408
|
-
|
|
409
|
-
if (args.timeout !== undefined && args.timeoutMs === undefined) args.timeoutMs = args.timeout * 1000;
|
|
410
417
|
const res = await invoke("bash", args);
|
|
411
418
|
|
|
412
|
-
if (res?.ok === false)
|
|
413
|
-
let exitCode;
|
|
414
|
-
|
|
415
|
-
try {
|
|
416
|
-
exitCode = JSON.parse(res.details).exitCode;
|
|
417
|
-
} catch {}
|
|
418
|
-
|
|
419
|
-
const output = String(res.value).trimEnd();
|
|
420
|
-
const suffix = Number.isInteger(exitCode) ? " (exit " + exitCode + ")" : "";
|
|
421
|
-
throw new Error("command failed" + suffix + ": " + command + (output ? "\n" + output : ""));
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
let text = unwrapValue(res);
|
|
425
|
-
|
|
426
|
-
if (res?.truncated && isString(text) && !text.includes("truncated")) text += "\n…[output truncated]…";
|
|
419
|
+
if (res?.ok === false) throw new Error(formatBashFailure(command, res));
|
|
427
420
|
|
|
428
|
-
return
|
|
421
|
+
return markTruncatedOutput(res, unwrapValue(res));
|
|
429
422
|
};
|
|
430
423
|
|
|
431
424
|
return { read, write, edit, bash, nova };
|
|
@@ -446,22 +439,7 @@ function makeConsole(runId, limits) {
|
|
|
446
439
|
return; }
|
|
447
440
|
|
|
448
441
|
count++;
|
|
449
|
-
|
|
450
|
-
const line = args
|
|
451
|
-
.map((a) => {
|
|
452
|
-
if (isString(a)) return a;
|
|
453
|
-
|
|
454
|
-
try {
|
|
455
|
-
const plain = toPlain(a);
|
|
456
|
-
const encoded = JSON.stringify(plain);
|
|
457
|
-
|
|
458
|
-
return encoded === undefined ? String(plain) : encoded;
|
|
459
|
-
} catch {
|
|
460
|
-
return String(a);
|
|
461
|
-
}
|
|
462
|
-
})
|
|
463
|
-
.join(" ");
|
|
464
|
-
|
|
442
|
+
const line = args.map(encodeConsoleArg).join(" ");
|
|
465
443
|
const clipped = truncateChars(line, limits.maxLogLineChars, "log");
|
|
466
444
|
|
|
467
445
|
if (clipped.truncated) markTruncated();
|
|
@@ -477,16 +455,55 @@ function postFailure(runId, err, location) {
|
|
|
477
455
|
post({ op: "error", runId, message, location });
|
|
478
456
|
}
|
|
479
457
|
|
|
458
|
+
function denyBuiltin(id) {
|
|
459
|
+
if (isDeniedGuestImport(id)) throw new Error(guestImportMessage(id));
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
let sealedRealm = false;
|
|
463
|
+
|
|
464
|
+
function sealGuestRealm() {
|
|
465
|
+
if (sealedRealm) return;
|
|
466
|
+
sealedRealm = true;
|
|
467
|
+
if (isFunction(process.getBuiltinModule)) {
|
|
468
|
+
const orig = process.getBuiltinModule.bind(process);
|
|
469
|
+
process.getBuiltinModule = (id) => {
|
|
470
|
+
denyBuiltin(id);
|
|
471
|
+
|
|
472
|
+
return orig(id);
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (isFunction(process.binding)) {
|
|
477
|
+
process.binding = (id) => {
|
|
478
|
+
throw new Error(guestImportMessage(String(id)));
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (isFunction(process.dlopen)) {
|
|
483
|
+
process.dlopen = () => {
|
|
484
|
+
throw new Error("guest cannot load native modules; use read, edit, write, or bash");
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (isFunction(process.kill)) {
|
|
489
|
+
// Signals are process-wide: process.kill escapes the worker thread and can
|
|
490
|
+
// terminate the host, so it stays out of the guest. Stop things with bash.
|
|
491
|
+
process.kill = () => {
|
|
492
|
+
throw new Error("process.kill is not available in guest programs; stop processes with bash");
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
480
497
|
async function handleRun(msg) {
|
|
481
498
|
const { runId, prepared, limits, available, batchRead = true } = msg;
|
|
482
499
|
activeRunId = runId;
|
|
483
500
|
runActive = true;
|
|
484
501
|
let compiled;
|
|
502
|
+
sealGuestRealm();
|
|
485
503
|
|
|
486
504
|
try {
|
|
487
505
|
// Existing programs may declare their own data variable; bind it only when supplied.
|
|
488
|
-
|
|
489
|
-
compiled = { fn: new AsyncFunction(...bindings, prepared.body), hasReturn: prepared.hasReturn };
|
|
506
|
+
compiled = compileGuest(prepared, msg.data);
|
|
490
507
|
} catch (err) {
|
|
491
508
|
postFailure(runId, new Error("JavaScript syntax error: " + err.message + "; no commands ran. When passing data, do not redeclare its binding."));
|
|
492
509
|
|
|
@@ -502,14 +519,7 @@ async function handleRun(msg) {
|
|
|
502
519
|
);
|
|
503
520
|
|
|
504
521
|
if (runId !== activeRunId) return;
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
try {
|
|
508
|
-
plain = toPlain(value);
|
|
509
|
-
} catch (err) {
|
|
510
|
-
plain = "[unserializable: " + (err?.message || err) + "]";
|
|
511
|
-
}
|
|
512
|
-
|
|
522
|
+
const plain = plainGuestValue(value);
|
|
513
523
|
runActive = false;
|
|
514
524
|
post({ op: "done", runId, value: plain, undefinedReturn: value === undefined && !compiled.hasReturn, hasReturn: compiled.hasReturn });
|
|
515
525
|
} catch (err) {
|
|
@@ -522,13 +532,7 @@ parentPort.on("message", (msg) => {
|
|
|
522
532
|
if (!isObject(msg)) return;
|
|
523
533
|
|
|
524
534
|
if (msg.op === "rpc:result") {
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
if (!pending) return;
|
|
528
|
-
pendingRpc.delete(msg.id);
|
|
529
|
-
|
|
530
|
-
if (msg.ok) pending.resolve(msg.value);
|
|
531
|
-
else pending.reject(new Error(msg.error));
|
|
535
|
+
handleRpcResult(msg);
|
|
532
536
|
|
|
533
537
|
return;
|
|
534
538
|
}
|