libfx 0.0.15 → 0.0.16
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 +8 -2
- package/browser-agent.js +21 -0
- package/browser-terminal.js +7 -0
- package/browser.js +12 -37
- package/bundled-asset.js +12 -0
- package/durable/local.mjs +3 -0
- package/durable/local.mjs.map +1 -0
- package/durable/vercel.mjs +3 -0
- package/durable/vercel.mjs.map +1 -0
- package/durable/world.mjs +3 -0
- package/durable/world.mjs.map +1 -0
- package/engine.js +2498 -0
- package/fx-core.wasm +0 -0
- package/fx-harness.js +1 -1
- package/fx-sdk.js +27 -3694
- package/fx-term.wasm +0 -0
- package/node-wasm.js +63 -0
- package/node.cjs +8 -8095
- package/node.js +94 -103
- package/package.json +10 -3
- package/types/node.d.ts +1 -1
- package/wasm-runtime.js +1049 -0
- package/wasm-terminal.js +80 -0
- package/xterm.js +112 -0
- package/libfx.darwin-arm64.node +0 -0
- package/libfx.darwin-x64.node +0 -0
- package/libfx.linux-arm64.node +0 -0
- package/libfx.linux-x64.node +0 -0
package/wasm-runtime.js
ADDED
|
@@ -0,0 +1,1049 @@
|
|
|
1
|
+
// The WebAssembly host: WASI, fx's host imports, and starting an fx
|
|
2
|
+
// WebAssembly module. Only the WebAssembly backends load it.
|
|
3
|
+
import { CoreOutput, maxCoreMessageBytes } from "./core-output.js";
|
|
4
|
+
import {
|
|
5
|
+
cancelResponseBody,
|
|
6
|
+
decoder,
|
|
7
|
+
encoder,
|
|
8
|
+
maxPendingAttachments,
|
|
9
|
+
maxSteeringMessageBytes,
|
|
10
|
+
maxSteeringMessages,
|
|
11
|
+
maxSteeringQueueBytes,
|
|
12
|
+
strictDecoder,
|
|
13
|
+
supportsJspi,
|
|
14
|
+
utf8Prefix,
|
|
15
|
+
} from "./engine.js";
|
|
16
|
+
import { loadModule } from "./wasm-module.js";
|
|
17
|
+
|
|
18
|
+
const workspaceInfoLimit = 4 * 1024;
|
|
19
|
+
const workspaceCommandLimit = 64 * 1024;
|
|
20
|
+
const workspaceOutputLimit = 64 * 1024;
|
|
21
|
+
// Matches max_instruction_file_bytes and max_instruction_path_bytes in js_host_workspace.zig.
|
|
22
|
+
const workspaceInstructionFileLimit = 1024 * 1024;
|
|
23
|
+
const workspaceInstructionPathLimit = 4 * 1024;
|
|
24
|
+
const workspaceReadTimeoutMs = 10_000;
|
|
25
|
+
const streamReadsPerTaskYield = 32;
|
|
26
|
+
const maxOutboundAttachments = 4;
|
|
27
|
+
|
|
28
|
+
function validWorkspacePath(path) {
|
|
29
|
+
if (typeof path !== "string" || !path.startsWith("/") || path.includes("\0")) return false;
|
|
30
|
+
if (strictDecoder.decode(encoder.encode(path)) !== path) return false;
|
|
31
|
+
if (path === "/") return true;
|
|
32
|
+
if (path.endsWith("/")) return false;
|
|
33
|
+
return path.slice(1).split("/").every((part) => part && part !== "." && part !== "..");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function workspacePathInside(root, path) {
|
|
37
|
+
return root === "/" ? path.startsWith("/") : path === root || path.startsWith(`${root}/`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// fx only reads AGENTS.md files below the workspace root or the host home.
|
|
41
|
+
function workspaceInstructionPathAllowed(info, path) {
|
|
42
|
+
return validWorkspacePath(path) && path.endsWith("/AGENTS.md") &&
|
|
43
|
+
(workspacePathInside(info.root, path) || workspacePathInside(info.home, path));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function prepareWorkspaceAdapter(workspace) {
|
|
47
|
+
if (workspace == null) return { present: false, valid: false };
|
|
48
|
+
try {
|
|
49
|
+
const info = workspace.info;
|
|
50
|
+
const permission = workspace.permission;
|
|
51
|
+
const readFile = workspace.readFile;
|
|
52
|
+
if (!info || typeof workspace.exec !== "function" || info.version !== 1 ||
|
|
53
|
+
(readFile !== undefined && typeof readFile !== "function") ||
|
|
54
|
+
!validWorkspacePath(info.root) || !validWorkspacePath(info.cwd) ||
|
|
55
|
+
!validWorkspacePath(info.home) || info.cwd !== info.root ||
|
|
56
|
+
info.gitAvailable !== false || info.ephemeral !== true ||
|
|
57
|
+
(permission !== "allow-sandboxed" && permission !== "prompt")) {
|
|
58
|
+
return { present: true, valid: false };
|
|
59
|
+
}
|
|
60
|
+
const value = {
|
|
61
|
+
version: 1,
|
|
62
|
+
root: info.root,
|
|
63
|
+
cwd: info.cwd,
|
|
64
|
+
home: info.home,
|
|
65
|
+
git: false,
|
|
66
|
+
ephemeral: true,
|
|
67
|
+
permission,
|
|
68
|
+
instruction_files: typeof readFile === "function",
|
|
69
|
+
};
|
|
70
|
+
const encoded = encoder.encode(JSON.stringify(value));
|
|
71
|
+
if (encoded.length > workspaceInfoLimit) return { present: true, valid: false };
|
|
72
|
+
return { present: true, valid: true, adapter: workspace, info: value, encoded };
|
|
73
|
+
} catch {
|
|
74
|
+
return { present: true, valid: false };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
class ByteQueue {
|
|
79
|
+
chunks = [];
|
|
80
|
+
waiters = [];
|
|
81
|
+
closed = false;
|
|
82
|
+
|
|
83
|
+
push(bytes) {
|
|
84
|
+
if (this.closed) throw new Error("fx runtime stdin is closed");
|
|
85
|
+
if (!bytes.length) return;
|
|
86
|
+
this.chunks.push(bytes);
|
|
87
|
+
this.wake();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
read(max) {
|
|
91
|
+
if (!this.chunks.length) return null;
|
|
92
|
+
const chunk = this.chunks[0];
|
|
93
|
+
const value = chunk.subarray(0, max);
|
|
94
|
+
if (value.length === chunk.length) this.chunks.shift();
|
|
95
|
+
else this.chunks[0] = chunk.subarray(value.length);
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
wait(timeoutMs) {
|
|
100
|
+
if (this.closed) return Promise.resolve(true);
|
|
101
|
+
return new Promise((resolve) => {
|
|
102
|
+
let settled = false;
|
|
103
|
+
let timer;
|
|
104
|
+
const waiter = () => {
|
|
105
|
+
if (settled) return;
|
|
106
|
+
settled = true;
|
|
107
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
108
|
+
resolve(true);
|
|
109
|
+
};
|
|
110
|
+
this.waiters.push(waiter);
|
|
111
|
+
if (timeoutMs !== undefined) {
|
|
112
|
+
timer = setTimeout(() => {
|
|
113
|
+
if (settled) return;
|
|
114
|
+
settled = true;
|
|
115
|
+
const index = this.waiters.indexOf(waiter);
|
|
116
|
+
if (index >= 0) this.waiters.splice(index, 1);
|
|
117
|
+
resolve(false);
|
|
118
|
+
}, timeoutMs);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
close() {
|
|
124
|
+
this.closed = true;
|
|
125
|
+
this.wake();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
wake() {
|
|
129
|
+
this.waiters.splice(0).forEach((resolve) => resolve());
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function raceWithTimeout(promise, timeoutMs, timeoutValue) {
|
|
134
|
+
let timer;
|
|
135
|
+
return new Promise((resolve, reject) => {
|
|
136
|
+
timer = setTimeout(() => resolve(timeoutValue), timeoutMs);
|
|
137
|
+
promise.then(
|
|
138
|
+
(value) => { clearTimeout(timer); resolve(value); },
|
|
139
|
+
(error) => { clearTimeout(timer); reject(error); },
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function yieldToHostTask() {
|
|
145
|
+
if (typeof globalThis.setImmediate === "function") {
|
|
146
|
+
return new Promise((resolve) => globalThis.setImmediate(resolve));
|
|
147
|
+
}
|
|
148
|
+
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function createRuntime(options) {
|
|
152
|
+
// Creating this inside a Wasm call would retain that instance through the error stack.
|
|
153
|
+
const abortReason = new DOMException("This operation was aborted", "AbortError");
|
|
154
|
+
const stdin = new ByteQueue();
|
|
155
|
+
const streams = new Map();
|
|
156
|
+
const httpRequests = new Set();
|
|
157
|
+
const steering = [];
|
|
158
|
+
let steeringBytes = 0;
|
|
159
|
+
let steeringOpen = false;
|
|
160
|
+
// A steer that cannot join the running turn waits to run next.
|
|
161
|
+
let steeringHandoff = false;
|
|
162
|
+
// Raw payloads beside ACP frames: the core copies inbound bytes into its
|
|
163
|
+
// own memory and publishes outbound bytes for the agent to take.
|
|
164
|
+
const inboundAttachments = new Map();
|
|
165
|
+
const outboundAttachments = new Map();
|
|
166
|
+
let nextOutboundAttachment = 1;
|
|
167
|
+
const workspaceOperations = new Set();
|
|
168
|
+
const workspace = prepareWorkspaceAdapter(options.workspace);
|
|
169
|
+
const args = ["fx", ...(options.args || [])];
|
|
170
|
+
const env = Object.entries(options.env || {}).map(([key, value]) => `${key}=${value}`);
|
|
171
|
+
let instance;
|
|
172
|
+
let nextHandle = 1;
|
|
173
|
+
let exitedResolve;
|
|
174
|
+
let exitCode = null;
|
|
175
|
+
let aborted = false;
|
|
176
|
+
let coreOutput;
|
|
177
|
+
let outputError;
|
|
178
|
+
const exited = new Promise((resolve) => { exitedResolve = resolve; });
|
|
179
|
+
const markExited = (code) => {
|
|
180
|
+
if (exitCode !== null) return;
|
|
181
|
+
exitCode = code;
|
|
182
|
+
exitedResolve(code);
|
|
183
|
+
};
|
|
184
|
+
const memory = () => instance.exports.memory;
|
|
185
|
+
const bytes = (ptr, len) => new Uint8Array(memory().buffer, ptr, len);
|
|
186
|
+
const text = (ptr, len) => decoder.decode(bytes(ptr, len));
|
|
187
|
+
const writeU32 = (ptr, value) => new DataView(memory().buffer).setUint32(ptr, value, true);
|
|
188
|
+
const writeU64 = (ptr, value) => new DataView(memory().buffer).setBigUint64(ptr, BigInt(value), true);
|
|
189
|
+
|
|
190
|
+
function checkedBytes(ptr, len) {
|
|
191
|
+
if (!Number.isInteger(ptr) || !Number.isInteger(len) || ptr < 0 || len < 0 ||
|
|
192
|
+
ptr > memory().buffer.byteLength || len > memory().buffer.byteLength - ptr) return null;
|
|
193
|
+
return bytes(ptr, len);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function writeVector(values, ptrs, data) {
|
|
197
|
+
let cursor = data;
|
|
198
|
+
values.forEach((value, index) => {
|
|
199
|
+
const encoded = encoder.encode(`${value}\0`);
|
|
200
|
+
writeU32(ptrs + index * 4, cursor);
|
|
201
|
+
bytes(cursor, encoded.length).set(encoded);
|
|
202
|
+
cursor += encoded.length;
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function emitStdout(chunk) {
|
|
207
|
+
if (options.stdout) return options.stdout(chunk);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function fdWrite(fd, iovs, count, nwritten) {
|
|
211
|
+
if (options.traceWasi) console.error("wasi fd_write", { fd, count });
|
|
212
|
+
const view = new DataView(memory().buffer);
|
|
213
|
+
let total = 0;
|
|
214
|
+
for (let index = 0; index < count; index++) {
|
|
215
|
+
total += view.getUint32(iovs + index * 8 + 4, true);
|
|
216
|
+
}
|
|
217
|
+
if (coreOutput && total > maxCoreMessageBytes) throw new RangeError("core output message exceeds 64 MiB");
|
|
218
|
+
if (fd === 1 || fd === 2) {
|
|
219
|
+
const chunk = new Uint8Array(total);
|
|
220
|
+
let offset = 0;
|
|
221
|
+
for (let index = 0; index < count; index++) {
|
|
222
|
+
const ptr = view.getUint32(iovs + index * 8, true);
|
|
223
|
+
const len = view.getUint32(iovs + index * 8 + 4, true);
|
|
224
|
+
chunk.set(bytes(ptr, len), offset);
|
|
225
|
+
offset += len;
|
|
226
|
+
}
|
|
227
|
+
if (fd === 1) {
|
|
228
|
+
const pending = emitStdout(chunk);
|
|
229
|
+
if (coreOutput && pending) return Promise.resolve(pending).then(() => {
|
|
230
|
+
writeU32(nwritten, total);
|
|
231
|
+
return 0;
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
else if (typeof options.stderr === "function") options.stderr(chunk);
|
|
235
|
+
else console.warn(decoder.decode(chunk));
|
|
236
|
+
}
|
|
237
|
+
writeU32(nwritten, total);
|
|
238
|
+
return 0;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function fdRead(fd, iovs, count, nread) {
|
|
242
|
+
if (fd !== 0) return 8;
|
|
243
|
+
const attempt = () => {
|
|
244
|
+
const view = new DataView(memory().buffer);
|
|
245
|
+
let total = 0;
|
|
246
|
+
for (let index = 0; index < count; index++) {
|
|
247
|
+
const ptr = view.getUint32(iovs + index * 8, true);
|
|
248
|
+
const len = view.getUint32(iovs + index * 8 + 4, true);
|
|
249
|
+
const chunk = stdin.read(len);
|
|
250
|
+
if (!chunk) break;
|
|
251
|
+
bytes(ptr, chunk.length).set(chunk);
|
|
252
|
+
total += chunk.length;
|
|
253
|
+
if (chunk.length < len) break;
|
|
254
|
+
}
|
|
255
|
+
if (total) { writeU32(nread, total); return 0; }
|
|
256
|
+
return null;
|
|
257
|
+
};
|
|
258
|
+
const immediate = attempt();
|
|
259
|
+
if (immediate !== null) return immediate;
|
|
260
|
+
if (stdin.closed) { writeU32(nread, 0); return 0; }
|
|
261
|
+
return stdin.wait().then(() => {
|
|
262
|
+
const result = attempt();
|
|
263
|
+
if (result !== null) return result;
|
|
264
|
+
writeU32(nread, 0);
|
|
265
|
+
return 0;
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function pollOneoff(subscriptions, events, count, nevents) {
|
|
270
|
+
const view = new DataView(memory().buffer);
|
|
271
|
+
for (let index = 0; index < count; index++) {
|
|
272
|
+
const base = subscriptions + index * 48;
|
|
273
|
+
const type = view.getUint8(base + 8);
|
|
274
|
+
if (type === 1 && stdin.chunks.length) {
|
|
275
|
+
bytes(events, 32).fill(0);
|
|
276
|
+
bytes(events, 8).set(bytes(base, 8));
|
|
277
|
+
view.setUint8(events + 10, 1);
|
|
278
|
+
writeU32(nevents, 1);
|
|
279
|
+
return 0;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
let timeout = null;
|
|
283
|
+
for (let index = 0; index < count; index++) {
|
|
284
|
+
const base = subscriptions + index * 48;
|
|
285
|
+
if (view.getUint8(base + 8) === 0) timeout = Number(view.getBigUint64(base + 24, true) / 1000000n);
|
|
286
|
+
}
|
|
287
|
+
return stdin.wait(timeout === null ? undefined : timeout).then(() => {
|
|
288
|
+
bytes(events, 32).fill(0);
|
|
289
|
+
bytes(events, 8).set(bytes(subscriptions, 8));
|
|
290
|
+
writeU32(nevents, 1);
|
|
291
|
+
return 0;
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function termPollInput(timeoutMs) {
|
|
296
|
+
options.onTerminalPoll?.();
|
|
297
|
+
if (stdin.chunks.length) return 1;
|
|
298
|
+
if (stdin.closed) return -1;
|
|
299
|
+
if (timeoutMs === 0) return 0;
|
|
300
|
+
return stdin.wait(timeoutMs >= 0 ? timeoutMs : undefined).then(() =>
|
|
301
|
+
stdin.chunks.length ? 1 : (stdin.closed ? -1 : 0));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function headersFromJson(ptr, len) {
|
|
305
|
+
const headers = new Headers();
|
|
306
|
+
for (const { name, value } of JSON.parse(text(ptr, len) || "[]")) headers.append(name, value);
|
|
307
|
+
return headers;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function streamOpen(methodPtr, methodLen, urlPtr, urlLen, headersPtr, headersLen, bodyPtr, bodyLen) {
|
|
311
|
+
const controller = new AbortController();
|
|
312
|
+
const handle = nextHandle++;
|
|
313
|
+
const state = {
|
|
314
|
+
controller,
|
|
315
|
+
reader: null,
|
|
316
|
+
leftover: new Uint8Array(),
|
|
317
|
+
response: null,
|
|
318
|
+
responseError: null,
|
|
319
|
+
responseSettled: null,
|
|
320
|
+
pendingRead: null,
|
|
321
|
+
readResult: null,
|
|
322
|
+
readError: null,
|
|
323
|
+
readsSinceTaskYield: 0,
|
|
324
|
+
};
|
|
325
|
+
streams.set(handle, state);
|
|
326
|
+
state.responseSettled = Promise.resolve().then(() => options.fetch(text(urlPtr, urlLen), {
|
|
327
|
+
method: text(methodPtr, methodLen),
|
|
328
|
+
headers: headersFromJson(headersPtr, headersLen),
|
|
329
|
+
body: bodyLen ? bytes(bodyPtr, bodyLen).slice() : undefined,
|
|
330
|
+
signal: controller.signal,
|
|
331
|
+
})).then((response) => {
|
|
332
|
+
state.response = response;
|
|
333
|
+
state.reader = response.body?.getReader() || null;
|
|
334
|
+
}).catch((error) => {
|
|
335
|
+
state.responseError = error;
|
|
336
|
+
});
|
|
337
|
+
return handle;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function streamStatus(handle, statusOut) {
|
|
341
|
+
const state = streams.get(handle);
|
|
342
|
+
if (!state) return -1;
|
|
343
|
+
const settled = () => {
|
|
344
|
+
if (state.responseError) return state.responseError?.name === "AbortError" ? -2 : -1;
|
|
345
|
+
if (!state.response) return 0;
|
|
346
|
+
new DataView(memory().buffer).setUint16(statusOut, state.response.status, true);
|
|
347
|
+
return 1;
|
|
348
|
+
};
|
|
349
|
+
const immediate = settled();
|
|
350
|
+
if (immediate !== 0) return immediate;
|
|
351
|
+
return raceWithTimeout(state.responseSettled.then(() => true), 50, false).then((ready) =>
|
|
352
|
+
ready ? settled() : 0);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function streamNext(handle, outPtr, outCap) {
|
|
356
|
+
const state = streams.get(handle);
|
|
357
|
+
if (!state) return -1;
|
|
358
|
+
const yieldAfterReadyResult = (result) => {
|
|
359
|
+
if (result <= 0) return result;
|
|
360
|
+
state.readsSinceTaskYield += 1;
|
|
361
|
+
if (state.readsSinceTaskYield < streamReadsPerTaskYield) return result;
|
|
362
|
+
state.readsSinceTaskYield = 0;
|
|
363
|
+
return yieldToHostTask().then(() => result);
|
|
364
|
+
};
|
|
365
|
+
const copy = (chunk) => {
|
|
366
|
+
const written = chunk.subarray(0, outCap);
|
|
367
|
+
bytes(outPtr, written.length).set(written);
|
|
368
|
+
state.leftover = chunk.subarray(written.length);
|
|
369
|
+
return written.length;
|
|
370
|
+
};
|
|
371
|
+
const consume = () => {
|
|
372
|
+
if (state.leftover.length) return copy(state.leftover);
|
|
373
|
+
if (state.readError) return state.readError?.name === "AbortError" ? -2 : -1;
|
|
374
|
+
if (!state.readResult) return null;
|
|
375
|
+
const { done, value } = state.readResult;
|
|
376
|
+
state.readResult = null;
|
|
377
|
+
if (done) return 0;
|
|
378
|
+
if (!value?.length) return null;
|
|
379
|
+
options.onTransportChunk?.(value.length);
|
|
380
|
+
return copy(value);
|
|
381
|
+
};
|
|
382
|
+
const immediate = consume();
|
|
383
|
+
if (immediate !== null) return yieldAfterReadyResult(immediate);
|
|
384
|
+
if (!state.reader) return 0;
|
|
385
|
+
if (!state.pendingRead) {
|
|
386
|
+
state.pendingRead = state.reader.read().then((result) => {
|
|
387
|
+
state.readResult = result;
|
|
388
|
+
state.pendingRead = null;
|
|
389
|
+
}).catch((error) => {
|
|
390
|
+
state.readError = error;
|
|
391
|
+
state.pendingRead = null;
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
return raceWithTimeout(state.pendingRead.then(() => true), 50, false).then((ready) => {
|
|
395
|
+
if (!ready) return -3;
|
|
396
|
+
const result = consume();
|
|
397
|
+
return result === null ? -3 : yieldAfterReadyResult(result);
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function httpRequest(methodPtr, methodLen, urlPtr, urlLen, headersPtr, headersLen, bodyPtr, bodyLen, statusOut, responsePtr, responseCap) {
|
|
402
|
+
const controller = new AbortController();
|
|
403
|
+
httpRequests.add(controller);
|
|
404
|
+
let onAbort;
|
|
405
|
+
const cancelled = new Promise((resolve) => {
|
|
406
|
+
onAbort = () => resolve(-1);
|
|
407
|
+
controller.signal.addEventListener("abort", onAbort, { once: true });
|
|
408
|
+
});
|
|
409
|
+
const request = (async () => {
|
|
410
|
+
const response = await options.fetch(text(urlPtr, urlLen), {
|
|
411
|
+
method: text(methodPtr, methodLen),
|
|
412
|
+
headers: headersFromJson(headersPtr, headersLen),
|
|
413
|
+
body: bodyLen ? bytes(bodyPtr, bodyLen).slice() : undefined,
|
|
414
|
+
signal: controller.signal,
|
|
415
|
+
});
|
|
416
|
+
if (controller.signal.aborted) {
|
|
417
|
+
void cancelResponseBody(response);
|
|
418
|
+
return -1;
|
|
419
|
+
}
|
|
420
|
+
const body = new Uint8Array(await response.arrayBuffer());
|
|
421
|
+
if (controller.signal.aborted) return -1;
|
|
422
|
+
new DataView(memory().buffer).setUint16(statusOut, response.status, true);
|
|
423
|
+
if (body.length > responseCap) return -2;
|
|
424
|
+
bytes(responsePtr, body.length).set(body);
|
|
425
|
+
return body.length;
|
|
426
|
+
})().catch(() => -1);
|
|
427
|
+
return Promise.race([request, cancelled]).finally(() => {
|
|
428
|
+
controller.signal.removeEventListener("abort", onAbort);
|
|
429
|
+
httpRequests.delete(controller);
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function clearSteering() {
|
|
434
|
+
steering.length = 0;
|
|
435
|
+
steeringBytes = 0;
|
|
436
|
+
steeringHandoff = false;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function queueSteering(text, id = "") {
|
|
440
|
+
if (!steeringOpen) throw new Error("no prompt is running");
|
|
441
|
+
const value = encoder.encode(text);
|
|
442
|
+
if (value.length === 0) throw new TypeError("steering text cannot be empty");
|
|
443
|
+
if (value.length > maxSteeringMessageBytes) {
|
|
444
|
+
throw new RangeError(`steering text exceeds the ${maxSteeringMessageBytes} byte libfx limit`);
|
|
445
|
+
}
|
|
446
|
+
if (steering.length >= maxSteeringMessages || value.length > maxSteeringQueueBytes - steeringBytes) {
|
|
447
|
+
throw new Error("steering queue is full");
|
|
448
|
+
}
|
|
449
|
+
steering.push({ id, value });
|
|
450
|
+
steeringBytes += value.length;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// The core takes queued steering at a boundary; until then it can be withdrawn.
|
|
454
|
+
function withdrawSteering(id) {
|
|
455
|
+
const index = steering.findIndex((entry) => entry.id === id);
|
|
456
|
+
if (index < 0) return false;
|
|
457
|
+
steeringBytes -= steering[index].value.length;
|
|
458
|
+
steering.splice(index, 1);
|
|
459
|
+
return true;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Each message is its input id, a newline, then the text.
|
|
463
|
+
function steeringTake(outputPtr, outputCap) {
|
|
464
|
+
const entry = steering[0];
|
|
465
|
+
if (!entry) return 0;
|
|
466
|
+
const output = checkedBytes(outputPtr, outputCap);
|
|
467
|
+
const id = encoder.encode(entry.id);
|
|
468
|
+
const length = id.length + 1 + entry.value.length;
|
|
469
|
+
if (!output || length > output.length) return -1;
|
|
470
|
+
output.set(id, 0);
|
|
471
|
+
output[id.length] = 10;
|
|
472
|
+
output.set(entry.value, id.length + 1);
|
|
473
|
+
steering.shift();
|
|
474
|
+
steeringBytes -= entry.value.length;
|
|
475
|
+
return length;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function writeAttachment(id, data) {
|
|
479
|
+
if (inboundAttachments.size >= maxPendingAttachments) throw new Error("attachment table is full");
|
|
480
|
+
inboundAttachments.set(id, data.slice());
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function attachmentSize(id) {
|
|
484
|
+
return inboundAttachments.get(id >>> 0)?.length ?? -1;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function attachmentTake(id, outputPtr, outputCap) {
|
|
488
|
+
const key = id >>> 0;
|
|
489
|
+
const value = inboundAttachments.get(key);
|
|
490
|
+
if (!value) return -1;
|
|
491
|
+
// An empty payload needs no output buffer, whose pointer may be arbitrary.
|
|
492
|
+
if (value.length > 0) {
|
|
493
|
+
const output = checkedBytes(outputPtr, outputCap);
|
|
494
|
+
if (!output || value.length > output.length) return -1;
|
|
495
|
+
output.set(value);
|
|
496
|
+
}
|
|
497
|
+
inboundAttachments.delete(key);
|
|
498
|
+
return value.length;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function attachmentPut(inputPtr, inputLen) {
|
|
502
|
+
const input = checkedBytes(inputPtr, inputLen);
|
|
503
|
+
if (!input || outboundAttachments.size >= maxOutboundAttachments) return -1;
|
|
504
|
+
const id = nextOutboundAttachment;
|
|
505
|
+
nextOutboundAttachment = id === 0x7fffffff ? 1 : id + 1;
|
|
506
|
+
outboundAttachments.set(id, input.slice());
|
|
507
|
+
return id;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Resolves once the agent's journal holds every event the core sent.
|
|
511
|
+
function journalFlush() {
|
|
512
|
+
if (typeof options.journalFlush !== "function") return -1;
|
|
513
|
+
return Promise.resolve().then(() => options.journalFlush()).then(() => 0, () => -1);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
let pendingHostToolResult = null;
|
|
517
|
+
function hostToolCall(namePtr, nameLen, argumentsPtr, argumentsLen, outputPtr, outputCap, statusPtr, callIdPtr, callIdLen) {
|
|
518
|
+
pendingHostToolResult = null;
|
|
519
|
+
if (typeof options.hostToolExecutor !== "function") return -1;
|
|
520
|
+
if (options.traceWasi) console.error("fx host tool call start");
|
|
521
|
+
let input;
|
|
522
|
+
try { input = JSON.parse(text(argumentsPtr, argumentsLen)); } catch { return -1; }
|
|
523
|
+
return Promise.resolve(options.hostToolExecutor(text(namePtr, nameLen), input, undefined, text(callIdPtr, callIdLen))).then((result) => {
|
|
524
|
+
if (options.traceWasi) console.error("fx host tool call settled", result.cancelled, result.isError);
|
|
525
|
+
if (result.cancelled) return -2;
|
|
526
|
+
const output = encoder.encode(result.content);
|
|
527
|
+
bytes(statusPtr, 1)[0] = (result.isError ? 1 : 0) + (result.rich ? 2 : 0);
|
|
528
|
+
if (output.length > outputCap) {
|
|
529
|
+
if (!result.rich || output.length > 8 * 1024 * 1024) return -3;
|
|
530
|
+
pendingHostToolResult = output;
|
|
531
|
+
return output.length;
|
|
532
|
+
}
|
|
533
|
+
bytes(outputPtr, output.length).set(output);
|
|
534
|
+
return output.length;
|
|
535
|
+
}).catch(() => -1);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function openUrl(urlPtr, urlLen) {
|
|
539
|
+
if (typeof options.openUrl !== "function") return 0;
|
|
540
|
+
return Promise.resolve().then(() => options.openUrl(text(urlPtr, urlLen))).then((accepted) =>
|
|
541
|
+
accepted === false ? 0 : 1).catch(() => 0);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function clipboardCopy(valuePtr, valueLen) {
|
|
545
|
+
let clipboard = options.clipboard;
|
|
546
|
+
if (clipboard === undefined) {
|
|
547
|
+
try { clipboard = globalThis.navigator?.clipboard; } catch { clipboard = null; }
|
|
548
|
+
}
|
|
549
|
+
if (typeof clipboard?.writeText !== "function") return Promise.resolve(0);
|
|
550
|
+
const value = text(valuePtr, valueLen);
|
|
551
|
+
return Promise.resolve().then(() => clipboard.writeText(value)).then((accepted) => {
|
|
552
|
+
if (accepted === false) return 0;
|
|
553
|
+
options.emit?.("clipboard.copy", { length: value.length });
|
|
554
|
+
return 1;
|
|
555
|
+
}).catch((error) => {
|
|
556
|
+
options.emit?.("clipboard.copy_error", { error });
|
|
557
|
+
return 0;
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function oauthSessionLoad(outPtr, outCap, revisionPtr, revisionCap, revisionLenOut) {
|
|
562
|
+
if (!options.oauthSessionStore?.load) return -1;
|
|
563
|
+
return Promise.resolve().then(() => options.oauthSessionStore.load()).then((record) => {
|
|
564
|
+
if (!record) return -2;
|
|
565
|
+
const value = record.bytes instanceof Uint8Array ? record.bytes : new Uint8Array(record.bytes);
|
|
566
|
+
if (typeof record.revision !== "string") return -1;
|
|
567
|
+
const revision = encoder.encode(record.revision);
|
|
568
|
+
if (value.length > outCap || revision.length > revisionCap) return -3;
|
|
569
|
+
bytes(outPtr, value.length).set(value);
|
|
570
|
+
bytes(revisionPtr, revision.length).set(revision);
|
|
571
|
+
writeU32(revisionLenOut, revision.length);
|
|
572
|
+
return value.length;
|
|
573
|
+
}).catch(() => -1);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function oauthSessionCommit(valuePtr, valueLen, expectedPtr, expectedLen, revisionPtr, revisionCap, revisionLenOut) {
|
|
577
|
+
if (!options.oauthSessionStore?.commit) return -1;
|
|
578
|
+
const expectedRevision = expectedLen ? text(expectedPtr, expectedLen) : undefined;
|
|
579
|
+
const value = bytes(valuePtr, valueLen).slice();
|
|
580
|
+
return Promise.resolve().then(() => options.oauthSessionStore.commit(value, expectedRevision)).then((result) => {
|
|
581
|
+
if (typeof result?.revision !== "string") return -1;
|
|
582
|
+
const revision = encoder.encode(result.revision);
|
|
583
|
+
if (revision.length > revisionCap) return -1;
|
|
584
|
+
bytes(revisionPtr, revision.length).set(revision);
|
|
585
|
+
writeU32(revisionLenOut, revision.length);
|
|
586
|
+
return 0;
|
|
587
|
+
}).catch((error) => error?.code === "FX_OAUTH_SESSION_REVISION_CONFLICT" ? -2 : -1);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function oauthSessionRemove(expectedPtr, expectedLen) {
|
|
591
|
+
if (!options.oauthSessionStore?.remove) return -1;
|
|
592
|
+
const expectedRevision = expectedLen ? text(expectedPtr, expectedLen) : undefined;
|
|
593
|
+
return Promise.resolve().then(() => options.oauthSessionStore.remove(expectedRevision)).then((result) =>
|
|
594
|
+
result === false || result === "missing" ? 1 : 0
|
|
595
|
+
).catch((error) => error?.code === "FX_OAUTH_SESSION_REVISION_CONFLICT" ? -2 : -1);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function configGet(idPtr, idLen, outPtr, outCap) {
|
|
599
|
+
if (!options.configStore?.get) return -2;
|
|
600
|
+
const configId = text(idPtr, idLen);
|
|
601
|
+
return Promise.resolve().then(() => options.configStore.get(configId)).then((value) => {
|
|
602
|
+
if (value === null || value === undefined) return -2;
|
|
603
|
+
if (typeof value !== "string") throw new TypeError("configStore.get() must return a string or null");
|
|
604
|
+
const encoded = encoder.encode(value);
|
|
605
|
+
if (encoded.length > outCap) return -3;
|
|
606
|
+
bytes(outPtr, encoded.length).set(encoded);
|
|
607
|
+
options.emit?.("config.restore", { configId, value });
|
|
608
|
+
return encoded.length;
|
|
609
|
+
}).catch((error) => {
|
|
610
|
+
options.emit?.("config.restore_error", { configId, error });
|
|
611
|
+
return -1;
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function configSet(idPtr, idLen, valuePtr, valueLen) {
|
|
616
|
+
if (!options.configStore?.set) return 0;
|
|
617
|
+
const configId = text(idPtr, idLen);
|
|
618
|
+
const value = text(valuePtr, valueLen);
|
|
619
|
+
return Promise.resolve().then(() => options.configStore.set(configId, value)).then(() => {
|
|
620
|
+
options.emit?.("config.changed", { configId, value, source: "terminal" });
|
|
621
|
+
return 0;
|
|
622
|
+
}).catch((error) => {
|
|
623
|
+
options.emit?.("config.persist_error", { configId, error });
|
|
624
|
+
return -1;
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function promptHistoryLoad(workspacePtr, workspaceLen, limit, outPtr, outCap) {
|
|
629
|
+
if (!options.promptHistoryStore?.load) return -1;
|
|
630
|
+
const workspaceRoot = text(workspacePtr, workspaceLen);
|
|
631
|
+
return Promise.resolve().then(() => options.promptHistoryStore.load(workspaceRoot, limit)).then((entries) => {
|
|
632
|
+
if (!Array.isArray(entries) || entries.some((entry) => typeof entry !== "string")) {
|
|
633
|
+
throw new TypeError("promptHistoryStore.load() must return an array of strings");
|
|
634
|
+
}
|
|
635
|
+
const value = encoder.encode(JSON.stringify(entries));
|
|
636
|
+
if (value.length > outCap) return -2;
|
|
637
|
+
bytes(outPtr, value.length).set(value);
|
|
638
|
+
options.emit?.("history.restore", { workspaceRoot, count: entries.length });
|
|
639
|
+
return value.length;
|
|
640
|
+
}).catch((error) => {
|
|
641
|
+
options.emit?.("history.restore_error", { workspaceRoot, error });
|
|
642
|
+
return -1;
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function promptHistoryAppend(timestampMs, workspacePtr, workspaceLen, valuePtr, valueLen) {
|
|
647
|
+
if (!options.promptHistoryStore?.append) return -1;
|
|
648
|
+
const workspaceRoot = text(workspacePtr, workspaceLen);
|
|
649
|
+
const value = text(valuePtr, valueLen);
|
|
650
|
+
return Promise.resolve().then(() => options.promptHistoryStore.append(workspaceRoot, value, Number(timestampMs))).then((result) => {
|
|
651
|
+
options.emit?.("history.append", { workspaceRoot });
|
|
652
|
+
if (result === "duplicate") return 1;
|
|
653
|
+
if (result === "record_too_large") return 2;
|
|
654
|
+
return 0;
|
|
655
|
+
}).catch((error) => {
|
|
656
|
+
options.emit?.("history.append_error", { workspaceRoot, error });
|
|
657
|
+
return -1;
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function promptHistoryClear(workspacePtr, workspaceLen) {
|
|
662
|
+
if (!options.promptHistoryStore?.clear) return -1;
|
|
663
|
+
const workspaceRoot = text(workspacePtr, workspaceLen);
|
|
664
|
+
return Promise.resolve().then(() => options.promptHistoryStore.clear(workspaceRoot)).then(() => {
|
|
665
|
+
options.emit?.("history.clear", { workspaceRoot });
|
|
666
|
+
return 0;
|
|
667
|
+
}).catch((error) => {
|
|
668
|
+
options.emit?.("history.clear_error", { workspaceRoot, error });
|
|
669
|
+
return -1;
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function sessionLoad(idPtr, idLen, outPtr, outCap, revisionPtr, revisionCap, revisionLenOut) {
|
|
674
|
+
if (!options.sessionStore) return -1;
|
|
675
|
+
return Promise.resolve().then(() => options.sessionStore.load(text(idPtr, idLen))).then((record) => {
|
|
676
|
+
if (!record) return -2;
|
|
677
|
+
const value = record.bytes instanceof Uint8Array ? record.bytes : new Uint8Array(record.bytes);
|
|
678
|
+
const revision = encoder.encode(record.revision);
|
|
679
|
+
if (value.length > outCap || revision.length > revisionCap) return -3;
|
|
680
|
+
bytes(outPtr, value.length).set(value);
|
|
681
|
+
bytes(revisionPtr, revision.length).set(revision);
|
|
682
|
+
writeU32(revisionLenOut, revision.length);
|
|
683
|
+
return value.length;
|
|
684
|
+
}).catch(() => -1);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function sessionCommit(idPtr, idLen, valuePtr, valueLen, expectedPtr, expectedLen, revisionPtr, revisionCap, revisionLenOut) {
|
|
688
|
+
if (!options.sessionStore) return -1;
|
|
689
|
+
const id = text(idPtr, idLen);
|
|
690
|
+
const expectedRevision = expectedLen ? text(expectedPtr, expectedLen) : undefined;
|
|
691
|
+
return Promise.resolve().then(() => options.sessionStore.commit(id, bytes(valuePtr, valueLen).slice(), expectedRevision)).then((result) => {
|
|
692
|
+
const revision = encoder.encode(result.revision);
|
|
693
|
+
if (revision.length > revisionCap) return -1;
|
|
694
|
+
bytes(revisionPtr, revision.length).set(revision);
|
|
695
|
+
writeU32(revisionLenOut, revision.length);
|
|
696
|
+
return 0;
|
|
697
|
+
}).catch((error) => error?.code === "FX_SESSION_REVISION_CONFLICT" ? -2 : -1);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function sessionList(outPtr, outCap) {
|
|
701
|
+
if (!options.sessionStore) return -1;
|
|
702
|
+
return Promise.resolve().then(() => options.sessionStore.list()).then((records) => {
|
|
703
|
+
const value = encoder.encode(JSON.stringify(records));
|
|
704
|
+
if (value.length > outCap) return -2;
|
|
705
|
+
bytes(outPtr, value.length).set(value);
|
|
706
|
+
return value.length;
|
|
707
|
+
}).catch(() => -1);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function sessionRemove(idPtr, idLen) {
|
|
711
|
+
if (!options.sessionStore) return -1;
|
|
712
|
+
return Promise.resolve().then(() => options.sessionStore.remove(text(idPtr, idLen))).then(() => 0).catch(() => -1);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function workspaceInfo(outPtr, outCap) {
|
|
716
|
+
if (!workspace.present) return -2;
|
|
717
|
+
if (!workspace.valid) return -4;
|
|
718
|
+
const output = checkedBytes(outPtr, outCap);
|
|
719
|
+
if (!output) return -4;
|
|
720
|
+
if (workspace.encoded.length > outCap) return -3;
|
|
721
|
+
output.set(workspace.encoded);
|
|
722
|
+
return workspace.encoded.length;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function workspaceExec(commandPtr, commandLen, timeoutMs, outputPtr, outputCap, resultPtr) {
|
|
726
|
+
if (!workspace.present) return Promise.resolve(-2);
|
|
727
|
+
if (!workspace.valid) return Promise.resolve(-4);
|
|
728
|
+
const commandBytes = checkedBytes(commandPtr, commandLen);
|
|
729
|
+
const output = checkedBytes(outputPtr, outputCap);
|
|
730
|
+
const resultBytes = checkedBytes(resultPtr, 32);
|
|
731
|
+
if (!commandBytes || !output || !resultBytes || commandLen > workspaceCommandLimit ||
|
|
732
|
+
outputCap > workspaceOutputLimit || !Number.isInteger(timeoutMs) ||
|
|
733
|
+
timeoutMs < 1 || timeoutMs > 30_000) return Promise.resolve(-4);
|
|
734
|
+
let command;
|
|
735
|
+
try {
|
|
736
|
+
command = strictDecoder.decode(commandBytes);
|
|
737
|
+
} catch {
|
|
738
|
+
return Promise.resolve(-4);
|
|
739
|
+
}
|
|
740
|
+
if (command.includes("\0")) return Promise.resolve(-4);
|
|
741
|
+
|
|
742
|
+
return runWorkspaceOperation(timeoutMs, "workspace command", (signal) => workspace.adapter.exec({
|
|
743
|
+
command,
|
|
744
|
+
cwd: workspace.info.cwd,
|
|
745
|
+
signal,
|
|
746
|
+
timeoutMs,
|
|
747
|
+
outputLimitBytes: workspaceOutputLimit,
|
|
748
|
+
}), (value) => {
|
|
749
|
+
if (!value || !Number.isInteger(value.exitCode) || value.exitCode < -0x80000000 ||
|
|
750
|
+
value.exitCode > 0x7fffffff || typeof value.stdout !== "string" ||
|
|
751
|
+
typeof value.stderr !== "string") return -1;
|
|
752
|
+
const stdout = encoder.encode(value.stdout);
|
|
753
|
+
const stderr = encoder.encode(value.stderr);
|
|
754
|
+
if (stdout.length > 0xffffffff || stderr.length > 0xffffffff) return -1;
|
|
755
|
+
|
|
756
|
+
let stdoutCap = stdout.length;
|
|
757
|
+
let stderrCap = stderr.length;
|
|
758
|
+
if (stdout.length + stderr.length > outputCap) {
|
|
759
|
+
stdoutCap = Math.min(stdout.length, Math.ceil(outputCap / 2));
|
|
760
|
+
stderrCap = Math.min(stderr.length, Math.floor(outputCap / 2));
|
|
761
|
+
let remaining = outputCap - stdoutCap - stderrCap;
|
|
762
|
+
const stdoutExtra = Math.min(remaining, stdout.length - stdoutCap);
|
|
763
|
+
stdoutCap += stdoutExtra;
|
|
764
|
+
remaining -= stdoutExtra;
|
|
765
|
+
stderrCap += Math.min(remaining, stderr.length - stderrCap);
|
|
766
|
+
}
|
|
767
|
+
const stdoutPreview = utf8Prefix(stdout, stdoutCap);
|
|
768
|
+
const stderrPreview = utf8Prefix(stderr, stderrCap);
|
|
769
|
+
output.set(stdoutPreview, 0);
|
|
770
|
+
output.set(stderrPreview, stdoutPreview.length);
|
|
771
|
+
const copied = stdoutPreview.length + stderrPreview.length;
|
|
772
|
+
const view = new DataView(memory().buffer, resultPtr, 32);
|
|
773
|
+
view.setInt32(0, value.exitCode, true);
|
|
774
|
+
view.setUint32(4, 0, true);
|
|
775
|
+
view.setUint32(8, stdoutPreview.length, true);
|
|
776
|
+
view.setUint32(12, stdout.length, true);
|
|
777
|
+
view.setUint32(16, stdoutPreview.length, true);
|
|
778
|
+
view.setUint32(20, stderrPreview.length, true);
|
|
779
|
+
view.setUint32(24, stderr.length, true);
|
|
780
|
+
view.setUint32(28, copied < stdout.length + stderr.length ? 1 : 0, true);
|
|
781
|
+
return 0;
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function workspaceReadFile(pathPtr, pathLen, outPtr, outCap, resultPtr) {
|
|
786
|
+
if (!workspace.present || (workspace.valid && typeof workspace.adapter.readFile !== "function")) {
|
|
787
|
+
return Promise.resolve(-2);
|
|
788
|
+
}
|
|
789
|
+
if (!workspace.valid) return Promise.resolve(-4);
|
|
790
|
+
const pathBytes = checkedBytes(pathPtr, pathLen);
|
|
791
|
+
if (!pathBytes || !checkedBytes(outPtr, outCap) || !checkedBytes(resultPtr, 8) ||
|
|
792
|
+
pathLen > workspaceInstructionPathLimit || outCap > workspaceInstructionFileLimit) {
|
|
793
|
+
return Promise.resolve(-4);
|
|
794
|
+
}
|
|
795
|
+
let path;
|
|
796
|
+
try {
|
|
797
|
+
path = strictDecoder.decode(pathBytes);
|
|
798
|
+
} catch {
|
|
799
|
+
return Promise.resolve(-4);
|
|
800
|
+
}
|
|
801
|
+
if (!workspaceInstructionPathAllowed(workspace.info, path)) return Promise.resolve(-4);
|
|
802
|
+
|
|
803
|
+
return runWorkspaceOperation(workspaceReadTimeoutMs, "workspace read", (signal) => workspace.adapter.readFile({
|
|
804
|
+
path,
|
|
805
|
+
signal,
|
|
806
|
+
}), (value) => {
|
|
807
|
+
if (value == null) return -6;
|
|
808
|
+
let content;
|
|
809
|
+
if (typeof value === "string") {
|
|
810
|
+
content = encoder.encode(value);
|
|
811
|
+
} else if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) {
|
|
812
|
+
content = value instanceof ArrayBuffer
|
|
813
|
+
? new Uint8Array(value)
|
|
814
|
+
: new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
815
|
+
try {
|
|
816
|
+
strictDecoder.decode(content);
|
|
817
|
+
} catch {
|
|
818
|
+
return -7;
|
|
819
|
+
}
|
|
820
|
+
} else {
|
|
821
|
+
return -1;
|
|
822
|
+
}
|
|
823
|
+
if (content.length > 0xffffffff) return -1;
|
|
824
|
+
const copied = Math.min(content.length, outCap);
|
|
825
|
+
// Reacquire views: memory may have grown while the host read was pending.
|
|
826
|
+
bytes(outPtr, copied).set(content.subarray(0, copied));
|
|
827
|
+
const view = new DataView(memory().buffer, resultPtr, 8);
|
|
828
|
+
view.setUint32(0, content.length, true);
|
|
829
|
+
view.setUint32(4, copied, true);
|
|
830
|
+
return 0;
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Runs one host workspace effect under a deadline and the shared Ctrl+C
|
|
835
|
+
// abort path. `finish` maps the host value to an ABI status.
|
|
836
|
+
function runWorkspaceOperation(timeoutMs, label, start, finish) {
|
|
837
|
+
const controller = new AbortController();
|
|
838
|
+
let resolveAbort;
|
|
839
|
+
const aborted = new Promise((resolve) => { resolveAbort = resolve; });
|
|
840
|
+
const state = {
|
|
841
|
+
status: null,
|
|
842
|
+
abort(status) {
|
|
843
|
+
if (this.status !== null) return;
|
|
844
|
+
this.status = status;
|
|
845
|
+
resolveAbort(status);
|
|
846
|
+
controller.abort(new DOMException(
|
|
847
|
+
status === -5 ? `${label} timed out` : `${label} aborted`,
|
|
848
|
+
status === -5 ? "TimeoutError" : "AbortError",
|
|
849
|
+
));
|
|
850
|
+
},
|
|
851
|
+
};
|
|
852
|
+
workspaceOperations.add(state);
|
|
853
|
+
const timer = setTimeout(() => state.abort(-5), timeoutMs);
|
|
854
|
+
const execution = Promise.resolve().then(() => start(controller.signal)).then((value) => {
|
|
855
|
+
if (state.status !== null) return state.status;
|
|
856
|
+
return finish(value);
|
|
857
|
+
}).catch((error) => {
|
|
858
|
+
if (state.status !== null) return state.status;
|
|
859
|
+
if (error?.name === "TimeoutError") return -5;
|
|
860
|
+
if (error?.name === "AbortError") return -3;
|
|
861
|
+
return -1;
|
|
862
|
+
});
|
|
863
|
+
return Promise.race([execution, aborted]).finally(() => {
|
|
864
|
+
clearTimeout(timer);
|
|
865
|
+
workspaceOperations.delete(state);
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function abortHostEffects() {
|
|
870
|
+
pendingHostToolResult = null;
|
|
871
|
+
streams.forEach((state) => state.controller.abort(abortReason));
|
|
872
|
+
httpRequests.forEach((controller) => controller.abort(abortReason));
|
|
873
|
+
workspaceOperations.forEach((state) => state.abort(-3));
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
const unavailable = () => 52;
|
|
877
|
+
const wasi = {
|
|
878
|
+
args_sizes_get(count, size) { if (options.traceWasi) console.error("wasi args_sizes_get"); writeU32(count, args.length); writeU32(size, args.reduce((n, v) => n + encoder.encode(v).length + 1, 0)); return 0; },
|
|
879
|
+
args_get(ptrs, data) { if (options.traceWasi) console.error("wasi args_get"); writeVector(args, ptrs, data); return 0; },
|
|
880
|
+
environ_sizes_get(count, size) { if (options.traceWasi) console.error("wasi environ_sizes_get"); writeU32(count, env.length); writeU32(size, env.reduce((n, v) => n + encoder.encode(v).length + 1, 0)); return 0; },
|
|
881
|
+
environ_get(ptrs, data) { if (options.traceWasi) console.error("wasi environ_get"); writeVector(env, ptrs, data); return 0; },
|
|
882
|
+
fd_write: options.args?.[0] === "acp" ? new WebAssembly.Suspending(fdWrite) : fdWrite,
|
|
883
|
+
fd_read: new WebAssembly.Suspending(fdRead),
|
|
884
|
+
fd_close() { return 0; },
|
|
885
|
+
fd_fdstat_get(fd, out) {
|
|
886
|
+
if (options.traceWasi) console.error("wasi fd_fdstat_get", fd);
|
|
887
|
+
bytes(out, 24).fill(0);
|
|
888
|
+
const view = new DataView(memory().buffer);
|
|
889
|
+
view.setUint8(out, fd <= 2 ? 2 : 0);
|
|
890
|
+
view.setBigUint64(out + 8, 0xffffffffffffffffn, true);
|
|
891
|
+
view.setBigUint64(out + 16, 0xffffffffffffffffn, true);
|
|
892
|
+
return 0;
|
|
893
|
+
},
|
|
894
|
+
fd_filestat_get: unavailable,
|
|
895
|
+
fd_filestat_set_size: unavailable,
|
|
896
|
+
fd_filestat_set_times: unavailable,
|
|
897
|
+
fd_pread: unavailable,
|
|
898
|
+
fd_prestat_get() { return 8; },
|
|
899
|
+
fd_prestat_dir_name: unavailable,
|
|
900
|
+
fd_pwrite: unavailable,
|
|
901
|
+
fd_readdir: unavailable,
|
|
902
|
+
fd_seek() { return 29; },
|
|
903
|
+
fd_sync() { return 0; },
|
|
904
|
+
clock_res_get(_id, out) { if (options.traceWasi) console.error("wasi clock_res_get"); writeU64(out, 1000000n); return 0; },
|
|
905
|
+
clock_time_get(_id, _precision, out) { if (options.traceWasi) console.error("wasi clock_time_get"); writeU64(out, BigInt(Date.now()) * 1000000n); return 0; },
|
|
906
|
+
path_create_directory: unavailable,
|
|
907
|
+
path_filestat_get: unavailable,
|
|
908
|
+
path_filestat_set_times: unavailable,
|
|
909
|
+
path_link: unavailable,
|
|
910
|
+
path_open: unavailable,
|
|
911
|
+
path_readlink: unavailable,
|
|
912
|
+
path_remove_directory: unavailable,
|
|
913
|
+
path_rename: unavailable,
|
|
914
|
+
path_symlink: unavailable,
|
|
915
|
+
path_unlink_file: unavailable,
|
|
916
|
+
random_get(ptr, len) { crypto.getRandomValues(bytes(ptr, len)); return 0; },
|
|
917
|
+
poll_oneoff: new WebAssembly.Suspending(pollOneoff),
|
|
918
|
+
proc_exit(code) { if (options.traceWasi) console.error("wasi proc_exit", code); markExited(code); throw new WebAssembly.RuntimeError(`proc_exit(${code})`); },
|
|
919
|
+
};
|
|
920
|
+
|
|
921
|
+
const fx = {
|
|
922
|
+
fx_term_poll_input: new WebAssembly.Suspending(termPollInput),
|
|
923
|
+
fx_clipboard_copy: new WebAssembly.Suspending(clipboardCopy),
|
|
924
|
+
fx_prompt_history_available() { return options.promptHistoryStore ? 1 : 0; },
|
|
925
|
+
fx_workspace_available() { return workspace.present ? 1 : 0; },
|
|
926
|
+
fx_workspace_info: workspaceInfo,
|
|
927
|
+
fx_workspace_exec: new WebAssembly.Suspending(workspaceExec),
|
|
928
|
+
fx_workspace_read_file: new WebAssembly.Suspending(workspaceReadFile),
|
|
929
|
+
fx_http_stream_open: streamOpen,
|
|
930
|
+
fx_http_stream_status: new WebAssembly.Suspending(streamStatus),
|
|
931
|
+
fx_http_stream_next: new WebAssembly.Suspending(streamNext),
|
|
932
|
+
fx_http_stream_close(handle) { const state = streams.get(handle); state?.controller.abort(abortReason); streams.delete(handle); },
|
|
933
|
+
fx_http_request: new WebAssembly.Suspending(httpRequest),
|
|
934
|
+
fx_host_tool_call: new WebAssembly.Suspending(hostToolCall),
|
|
935
|
+
fx_journal_flush: new WebAssembly.Suspending(journalFlush),
|
|
936
|
+
fx_host_tool_result_read(offset, ptr, cap) {
|
|
937
|
+
if (!pendingHostToolResult || offset < 0 || offset > pendingHostToolResult.length) return -1;
|
|
938
|
+
const chunk = pendingHostToolResult.subarray(offset, offset + cap);
|
|
939
|
+
bytes(ptr, chunk.length).set(chunk);
|
|
940
|
+
return chunk.length;
|
|
941
|
+
},
|
|
942
|
+
fx_host_tool_result_release() { pendingHostToolResult = null; },
|
|
943
|
+
fx_steering_take: steeringTake,
|
|
944
|
+
fx_steering_close() { steeringOpen = false; clearSteering(); },
|
|
945
|
+
// -1 once the turn takes no more steering, else twice how many steers
|
|
946
|
+
// wait, plus one when a handoff does.
|
|
947
|
+
fx_steering_state() { return steeringOpen ? steering.length * 2 + (steeringHandoff ? 1 : 0) : -1; },
|
|
948
|
+
fx_attachment_size: attachmentSize,
|
|
949
|
+
fx_attachment_take: attachmentTake,
|
|
950
|
+
fx_attachment_put: attachmentPut,
|
|
951
|
+
fx_open_url: new WebAssembly.Suspending(openUrl),
|
|
952
|
+
fx_oauth_session_load: new WebAssembly.Suspending(oauthSessionLoad),
|
|
953
|
+
fx_oauth_session_commit: new WebAssembly.Suspending(oauthSessionCommit),
|
|
954
|
+
fx_oauth_session_remove: new WebAssembly.Suspending(oauthSessionRemove),
|
|
955
|
+
fx_config_get: new WebAssembly.Suspending(configGet),
|
|
956
|
+
fx_config_set: new WebAssembly.Suspending(configSet),
|
|
957
|
+
fx_prompt_history_load: new WebAssembly.Suspending(promptHistoryLoad),
|
|
958
|
+
fx_prompt_history_append: new WebAssembly.Suspending(promptHistoryAppend),
|
|
959
|
+
fx_prompt_history_clear: new WebAssembly.Suspending(promptHistoryClear),
|
|
960
|
+
fx_session_load: new WebAssembly.Suspending(sessionLoad),
|
|
961
|
+
fx_session_commit: new WebAssembly.Suspending(sessionCommit),
|
|
962
|
+
fx_session_list: new WebAssembly.Suspending(sessionList),
|
|
963
|
+
fx_session_remove: new WebAssembly.Suspending(sessionRemove),
|
|
964
|
+
fx_term_size(cols, rows) {
|
|
965
|
+
const width = options.terminal?.cols || 80;
|
|
966
|
+
const height = options.terminal?.rows || 24;
|
|
967
|
+
new DataView(memory().buffer).setUint16(cols, width, true);
|
|
968
|
+
new DataView(memory().buffer).setUint16(rows, height, true);
|
|
969
|
+
options.emit?.("terminal.size", { cols: width, rows: height });
|
|
970
|
+
},
|
|
971
|
+
};
|
|
972
|
+
|
|
973
|
+
return {
|
|
974
|
+
imports: { wasi_snapshot_preview1: wasi, fx }, exited,
|
|
975
|
+
setInstance(value) { instance = value; },
|
|
976
|
+
write(data) { stdin.push(typeof data === "string" ? encoder.encode(data) : data); },
|
|
977
|
+
writeAttachment,
|
|
978
|
+
takeAttachment(id) {
|
|
979
|
+
const value = outboundAttachments.get(id) ?? null;
|
|
980
|
+
outboundAttachments.delete(id);
|
|
981
|
+
return value;
|
|
982
|
+
},
|
|
983
|
+
discardAttachments() { inboundAttachments.clear(); },
|
|
984
|
+
wake() { stdin.wake(); },
|
|
985
|
+
closeStdin() { steeringOpen = false; clearSteering(); stdin.close(); },
|
|
986
|
+
openSteering() { clearSteering(); steeringOpen = true; },
|
|
987
|
+
steer: queueSteering,
|
|
988
|
+
withdrawSteering,
|
|
989
|
+
// Ends the running turn at its next safe point; false when none runs.
|
|
990
|
+
handOffSteering() {
|
|
991
|
+
if (!steeringOpen) return false;
|
|
992
|
+
steeringHandoff = true;
|
|
993
|
+
return true;
|
|
994
|
+
},
|
|
995
|
+
closeSteering() { steeringOpen = false; clearSteering(); },
|
|
996
|
+
abortHostEffects,
|
|
997
|
+
abort(error) {
|
|
998
|
+
aborted = true;
|
|
999
|
+
outputError = error;
|
|
1000
|
+
coreOutput?.close();
|
|
1001
|
+
abortHostEffects();
|
|
1002
|
+
stdin.close();
|
|
1003
|
+
markExited(130);
|
|
1004
|
+
},
|
|
1005
|
+
markExited,
|
|
1006
|
+
get aborted() { return aborted; },
|
|
1007
|
+
get exitCode() { return exitCode; },
|
|
1008
|
+
get error() { return outputError; },
|
|
1009
|
+
setLineHandler(handler) {
|
|
1010
|
+
coreOutput = new CoreOutput(handler);
|
|
1011
|
+
options.stdout = (chunk) => coreOutput.write(chunk);
|
|
1012
|
+
},
|
|
1013
|
+
finishOutput() { coreOutput?.finish(); },
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
export async function instantiate(options) {
|
|
1018
|
+
if (!supportsJspi()) throw new Error("fx WebAssembly requires JSPI (Chrome or Edge 137+)");
|
|
1019
|
+
const runtime = createRuntime({ fetch: globalThis.fetch.bind(globalThis), ...options });
|
|
1020
|
+
const module = await loadModule(options.wasm);
|
|
1021
|
+
const instance = await WebAssembly.instantiate(module, runtime.imports);
|
|
1022
|
+
runtime.setInstance(instance);
|
|
1023
|
+
const start = WebAssembly.promising(instance.exports._start);
|
|
1024
|
+
start().then(
|
|
1025
|
+
() => {
|
|
1026
|
+
runtime.setInstance(null);
|
|
1027
|
+
try { runtime.finishOutput(); runtime.markExited(0); }
|
|
1028
|
+
catch (error) { runtime.abort(error); }
|
|
1029
|
+
},
|
|
1030
|
+
(error) => {
|
|
1031
|
+
runtime.setInstance(null);
|
|
1032
|
+
if (options.args?.[0] === "acp" && !String(error).includes("proc_exit")) runtime.abort(error);
|
|
1033
|
+
else {
|
|
1034
|
+
if (!String(error).includes("proc_exit")) {
|
|
1035
|
+
runtime.abortHostEffects();
|
|
1036
|
+
console.error(error);
|
|
1037
|
+
}
|
|
1038
|
+
runtime.markExited(runtime.aborted ? 130 : 1);
|
|
1039
|
+
}
|
|
1040
|
+
},
|
|
1041
|
+
);
|
|
1042
|
+
return runtime;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// The runtime factory for an engine on WebAssembly. `wasm` is the fx-core
|
|
1046
|
+
// module; without it, each engine's own `wasm` option names it.
|
|
1047
|
+
export function wasmRuntimeFactory(wasm) {
|
|
1048
|
+
return (runtimeOptions) => instantiate(wasm === undefined ? runtimeOptions : { ...runtimeOptions, wasm });
|
|
1049
|
+
}
|