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