@fungi.computer/truffle 0.1.1 → 0.2.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 +282 -34
- package/dist/auth-cli.d.ts +10 -0
- package/dist/auth-cli.js +36 -14
- package/dist/fungi-account-command.d.ts +1 -1
- package/dist/fungi-account-command.js +6 -3
- package/dist/index.d.ts +10 -2
- package/dist/index.js +8 -2
- package/dist/internal/image-fit-metafile.json +106 -0
- package/dist/internal/local-image-fit-worker.mjs +960 -0
- package/dist/internal/private-workerd-sandbox-worker.mjs +11 -12
- package/dist/internal/sandbox-metafile.json +7 -7
- package/dist/local-archive-handoff.d.ts +12 -0
- package/dist/local-archive-handoff.js +21 -0
- package/dist/local-extensions.d.ts +58 -12
- package/dist/local-extensions.js +195 -55
- package/dist/local-image-fit.d.ts +30 -0
- package/dist/local-image-fit.js +105 -0
- package/dist/local-models.d.ts +4 -2
- package/dist/local-models.js +11 -6
- package/dist/local-session-archives.d.ts +8 -0
- package/dist/local-session-archives.js +152 -0
- package/dist/local-skill-source.d.ts +18 -3
- package/dist/local-skill-source.js +216 -140
- package/dist/log-event.d.ts +7 -0
- package/dist/log-event.js +8 -4
- package/dist/private-bounded-response.d.ts +2 -0
- package/dist/private-bounded-response.js +41 -0
- package/dist/private-file-credential-store.d.ts +28 -3
- package/dist/private-file-credential-store.js +57 -9
- package/dist/private-files-patch.d.ts +2 -1
- package/dist/private-files-patch.js +21 -3
- package/dist/private-fungi-account.d.ts +40 -23
- package/dist/private-fungi-account.js +201 -21
- package/dist/private-fungi-mcp-connection.d.ts +4 -0
- package/dist/private-fungi-mcp-connection.js +25 -6
- package/dist/private-fungi-mcp-selection-schema.d.ts +7 -0
- package/dist/private-fungi-mcp-selection-schema.js +15 -0
- package/dist/private-fungi-mcp-selection.d.ts +6 -6
- package/dist/private-fungi-mcp-selection.js +29 -16
- package/dist/private-fungi-model-api.js +4 -4
- package/dist/private-fungi-model-catalog.d.ts +10 -0
- package/dist/private-fungi-model-catalog.js +10 -2
- package/dist/private-fungi-model-provider.d.ts +1 -1
- package/dist/private-fungi-model-provider.js +6 -1
- package/dist/private-fungi-pending-logouts.d.ts +1 -1
- package/dist/private-fungi-pending-logouts.js +1 -2
- package/dist/private-fungi-service-logout.d.ts +44 -0
- package/dist/private-fungi-service-logout.js +59 -0
- package/dist/private-invocation-computer.d.ts +24 -3
- package/dist/private-invocation-computer.js +101 -62
- package/dist/private-invocation-files.d.ts +16 -1
- package/dist/private-invocation-files.js +154 -83
- package/dist/private-local-acp-stdio.js +3 -0
- package/dist/private-local-admission-gate.d.ts +14 -0
- package/dist/private-local-admission-gate.js +52 -0
- package/dist/private-local-agent-control.d.ts +101 -0
- package/dist/private-local-agent-control.js +30 -0
- package/dist/private-local-agent-default-model.d.ts +12 -0
- package/dist/private-local-agent-default-model.js +31 -0
- package/dist/private-local-auth-rpc.d.ts +67 -7
- package/dist/private-local-auth-rpc.js +140 -17
- package/dist/private-local-bwrap.d.ts +13 -0
- package/dist/private-local-bwrap.js +70 -0
- package/dist/private-local-client-version.d.ts +5 -0
- package/dist/private-local-client-version.js +43 -0
- package/dist/private-local-computer-attach.d.ts +3 -3
- package/dist/private-local-computer-attach.js +17 -3
- package/dist/private-local-computer-processes.d.ts +6 -3
- package/dist/private-local-computer-processes.js +55 -31
- package/dist/private-local-config-root.d.ts +21 -0
- package/dist/private-local-config-root.js +19 -0
- package/dist/private-local-containment.d.ts +51 -0
- package/dist/private-local-containment.js +161 -0
- package/dist/private-local-delegation.d.ts +10 -3
- package/dist/private-local-delegation.js +70 -15
- package/dist/private-local-effective-model.d.ts +9 -1
- package/dist/private-local-effective-model.js +3 -0
- package/dist/private-local-exec-environment.d.ts +16 -0
- package/dist/private-local-exec-environment.js +11 -0
- package/dist/private-local-linux-sandbox.d.ts +8 -0
- package/dist/private-local-linux-sandbox.js +67 -0
- package/dist/private-local-mail.d.ts +4 -2
- package/dist/private-local-mail.js +41 -33
- package/dist/private-local-mesh-enrollment.d.ts +34 -0
- package/dist/private-local-mesh-enrollment.js +188 -0
- package/dist/private-local-mesh-links.d.ts +21 -0
- package/dist/private-local-mesh-links.js +229 -0
- package/dist/private-local-mesh-record.d.ts +120 -0
- package/dist/private-local-mesh-record.js +55 -0
- package/dist/private-local-process-identity.d.ts +20 -0
- package/dist/private-local-process-identity.js +40 -0
- package/dist/private-local-runtime-host.d.ts +64 -13
- package/dist/private-local-runtime-host.js +361 -172
- package/dist/private-local-sandbox-policy.d.ts +21 -0
- package/dist/private-local-sandbox-policy.js +121 -0
- package/dist/private-local-sandbox.d.ts +37 -0
- package/dist/private-local-sandbox.js +114 -0
- package/dist/private-local-seatbelt.d.ts +24 -0
- package/dist/private-local-seatbelt.js +68 -0
- package/dist/private-local-service-contract.d.ts +91 -11
- package/dist/private-local-service-discovery.d.ts +129 -13
- package/dist/private-local-service-discovery.js +148 -47
- package/dist/private-local-service-lease.d.ts +16 -0
- package/dist/private-local-service-lease.js +105 -0
- package/dist/private-local-service-listener.d.ts +9 -2
- package/dist/private-local-service-listener.js +260 -64
- package/dist/private-local-service-log.d.ts +128 -0
- package/dist/private-local-service-log.js +321 -0
- package/dist/private-local-service-owner.d.ts +87 -1519
- package/dist/private-local-service-owner.js +1069 -71
- package/dist/private-local-service-process.js +3 -1
- package/dist/private-local-service-termination.d.ts +7 -0
- package/dist/private-local-service-termination.js +67 -0
- package/dist/private-local-service.d.ts +31 -2
- package/dist/private-local-service.js +402 -126
- package/dist/private-local-session-discovery.d.ts +52 -5
- package/dist/private-local-session-discovery.js +132 -11
- package/dist/private-local-shiitake-host.js +2 -2
- package/dist/private-local-startup-model.d.ts +51 -0
- package/dist/private-local-startup-model.js +86 -0
- package/dist/private-local-stop-all.d.ts +27 -0
- package/dist/private-local-stop-all.js +26 -0
- package/dist/private-local-ui-attachment.d.ts +19 -0
- package/dist/private-local-ui-attachment.js +71 -0
- package/dist/private-local-worker-server.d.ts +1 -0
- package/dist/private-local-worker-server.js +13 -5
- package/dist/private-mcp-form-contract.d.ts +186 -0
- package/dist/private-mcp-form-contract.js +53 -0
- package/dist/private-mcp-form-elicitation.d.ts +18 -0
- package/dist/private-mcp-form-elicitation.js +97 -0
- package/dist/private-mcp-form-session.d.ts +172 -0
- package/dist/private-mcp-form-session.js +148 -0
- package/dist/private-openai-claims.d.ts +30 -0
- package/dist/private-openai-claims.js +69 -0
- package/dist/private-oyster-mcp-auth.d.ts +13 -0
- package/dist/private-oyster-mcp-auth.js +57 -0
- package/dist/private-provider-rejection.d.ts +15 -0
- package/dist/private-provider-rejection.js +24 -0
- package/dist/private-sqlite-owner.d.ts +34 -2
- package/dist/private-sqlite-owner.js +103 -37
- package/dist/private-team-computers.d.ts +4 -4
- package/dist/private-team-computers.js +11 -2
- package/dist/seatbelt/safe-base.sbpl +79 -0
- package/dist/subscription-activation.d.ts +9 -0
- package/dist/subscription-activation.js +62 -0
- package/dist/subscription-identity.d.ts +23 -0
- package/dist/subscription-identity.js +124 -0
- package/dist/subscription-turn.d.ts +17 -0
- package/dist/subscription-turn.js +143 -0
- package/dist/team-apps.d.ts +2 -2
- package/dist/team-apps.js +7 -2
- package/native/bin/LICENSE.bubblewrap +502 -0
- package/native/bin/LICENSE.libcap +112 -0
- package/native/bin/LICENSE.sandbox-runtime +201 -0
- package/native/bin/SOURCE.bubblewrap.tar.xz +0 -0
- package/native/bin/bwrap-build-source/Dockerfile +48 -0
- package/native/bin/bwrap-build-source/README.md +96 -0
- package/native/bin/bwrap-build-source/build.mjs +52 -0
- package/native/bin/bwrap-build-source/linux-arm64.ini +14 -0
- package/native/bin/bwrap-build-source/pin.mjs +21 -0
- package/native/bin/bwrap-build-source/record.mjs +74 -0
- package/native/bin/bwrap-build-source/vendor/LICENSE.sandbox-runtime +201 -0
- package/native/bin/bwrap-build-source/vendor/README.md +23 -0
- package/native/bin/bwrap-build-source/vendor/seccomp-unix-block.c +280 -0
- package/native/bin/bwrap-build.json +44 -0
- package/native/bin/fungi-bwrap +0 -0
- package/native/bin/fungi-bwrap.sha256 +1 -0
- package/native/bin/fungi-seccomp.bpf +0 -0
- package/native/bin/fungi-seccomp.bpf.sha256 +1 -0
- package/package.json +44 -31
- package/dist/private-local-model-settings.d.ts +0 -5
- package/dist/private-local-model-settings.js +0 -31
- package/dist/private-local-subagents.d.ts +0 -27
- package/dist/private-local-subagents.js +0 -77
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { realpath } from "node:fs/promises";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
2
|
import { relative, resolve, sep } from "node:path";
|
|
4
3
|
import process from "node:process";
|
|
5
4
|
import * as ChildProcess from "effect/process/ChildProcess";
|
|
@@ -18,7 +17,12 @@ import { z } from "zod";
|
|
|
18
17
|
import { backgroundExecInput, backgroundExecOutput, callBackgroundExec, } from "./private-local-computer-processes.js";
|
|
19
18
|
import { capturePrivateLocalExecPath, parsePrivateLocalExecPath, } from "./private-local-exec-path.js";
|
|
20
19
|
import { combinedArgvBytes, launchCwdSchema, launchStdinSchema, maximumLaunchTextLength, } from "./private-local-exec-launch.js";
|
|
20
|
+
import { privateLocalExecEnvironment } from "./private-local-exec-environment.js";
|
|
21
|
+
import { capturePrivateLocalConfigRoot } from "./private-local-config-root.js";
|
|
21
22
|
const maximumStreamBytes = 64 * 1_024;
|
|
23
|
+
/** Room inside the stream cap for the visible truncation marker. */
|
|
24
|
+
const truncationMarkerReserveBytes = 256;
|
|
25
|
+
const keptStreamBytes = maximumStreamBytes - truncationMarkerReserveBytes;
|
|
22
26
|
const defaultCommandTimeoutMs = 180_000;
|
|
23
27
|
/**
|
|
24
28
|
* Keep in sync with the invocation deadline `private-local-runtime-host.ts`
|
|
@@ -78,11 +82,16 @@ const directRunInput = z
|
|
|
78
82
|
* branch and the union is refused.
|
|
79
83
|
*/
|
|
80
84
|
const execInput = z.union([shellRunInput, directRunInput]);
|
|
85
|
+
const execTruncation = z.strictObject({
|
|
86
|
+
stream: z.enum(["stdout", "stderr"]),
|
|
87
|
+
keptBytes: z.number().int().nonnegative(),
|
|
88
|
+
totalBytes: z.number().int().positive(),
|
|
89
|
+
});
|
|
81
90
|
const commandOutputFields = {
|
|
82
91
|
exitCode: z.number().int(),
|
|
83
92
|
stdout: z.string(),
|
|
84
93
|
stderr: z.string(),
|
|
85
|
-
truncated: z.
|
|
94
|
+
truncated: z.union([z.literal(false), z.array(execTruncation).min(1)]),
|
|
86
95
|
};
|
|
87
96
|
const execOutput = z.discriminatedUnion("status", [
|
|
88
97
|
z.strictObject({
|
|
@@ -105,14 +114,10 @@ function computerCapabilities(root, processes) {
|
|
|
105
114
|
id: "exec",
|
|
106
115
|
name: "exec",
|
|
107
116
|
kind: "runtime",
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
description: `Shell, terminal, git, builds and checks. Computer root: ${root}. cwd must be absolute and remain inside this root; omission starts at the root. ~/$HOME use home. Or {executable,args}; stdin once, closed. exec stops its group, including nohup. timeoutMs default ${String(defaultCommandTimeoutMs)}, max ${String(maximumCommandTimeoutMs)} ms; invocation ${String(invocationDeadlineMs)} ms. Deadline returns partial output.${processes === undefined ? "" : ' Background start takes command or executable+args; {operation:"start",command} returns process.id; poll/wait: state, exitCode, output tail. wait <=60000 ms (default 10000); timedOut means wait expired. Cancelling wait keeps jobs alive. maxBytes <=64 KiB with offset/nextOffset; retain last 8 MiB (logStartOffset). kill stops group; list shows Session jobs. Delete/close stop jobs; restart drops IDs. Use wait, not sleep.'}`,
|
|
113
|
-
parameters: structuredClone(z.toJSONSchema(processes === undefined ? execInput : shellInput, {
|
|
114
|
-
io: "input",
|
|
115
|
-
})),
|
|
117
|
+
// A filesystem path has no place in the bounded discovery summary.
|
|
118
|
+
// Publish the exact root in the input schema, without truncating it.
|
|
119
|
+
description: `Shell, terminal, git, builds. cwd uses the root in the input schema. ~/$HOME use home. Or {executable,args}; stdin once, closed. exec kills its group at call end; nohup/& die. timeoutMs default ${String(defaultCommandTimeoutMs)}, max ${String(maximumCommandTimeoutMs)} ms; invocation ${String(invocationDeadlineMs)} ms; deadline returns partial output. Streams keep 64 KiB, then a [stdout|stderr truncated] line; redirect to a file or head/tail.${processes === undefined ? "" : ' Jobs: {operation:"start",command} or executable+args returns process.id; poll/wait: state, exitCode, tail. wait <=60000 ms; timedOut: wait expired. Cancelled wait keeps jobs. maxBytes <=64 KiB, offset/nextOffset; keeps last 8 MiB (logStartOffset). kill stops group; list: Session jobs. Delete/close stop jobs. Use wait, not sleep.'}`,
|
|
120
|
+
parameters: structuredClone(z.toJSONSchema((processes === undefined ? execInput : shellInput).describe(`Computer root: ${root}. cwd must be absolute and remain inside this root; omission starts at the root.`), { io: "input" })),
|
|
116
121
|
output: structuredClone(z.toJSONSchema(processes === undefined ? execOutput : shellOutput)),
|
|
117
122
|
},
|
|
118
123
|
]);
|
|
@@ -242,52 +247,70 @@ function nativeGroupFinalizer(process) {
|
|
|
242
247
|
})));
|
|
243
248
|
}
|
|
244
249
|
function captureBoundedOutput(stream) {
|
|
245
|
-
const initial = {
|
|
246
|
-
chunks: [],
|
|
247
|
-
byteLength: 0,
|
|
248
|
-
truncated: false,
|
|
249
|
-
};
|
|
250
|
+
const initial = { chunks: [], byteLength: 0, totalBytes: 0 };
|
|
250
251
|
return Stream.runFold(stream, () => initial, (captured, chunk) => {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
chunks: [...captured.chunks, chunk],
|
|
257
|
-
byteLength: captured.byteLength + chunk.byteLength,
|
|
258
|
-
truncated: false,
|
|
259
|
-
};
|
|
260
|
-
}
|
|
252
|
+
const remaining = keptStreamBytes - captured.byteLength;
|
|
253
|
+
const totalBytes = captured.totalBytes + chunk.byteLength;
|
|
254
|
+
if (remaining <= 0)
|
|
255
|
+
return { ...captured, totalBytes };
|
|
256
|
+
const kept = chunk.byteLength <= remaining ? chunk : chunk.slice(0, remaining);
|
|
261
257
|
return {
|
|
262
|
-
chunks:
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
byteLength: maximumStreamBytes,
|
|
266
|
-
truncated: true,
|
|
258
|
+
chunks: [...captured.chunks, kept],
|
|
259
|
+
byteLength: captured.byteLength + kept.byteLength,
|
|
260
|
+
totalBytes,
|
|
267
261
|
};
|
|
268
262
|
});
|
|
269
263
|
}
|
|
270
|
-
|
|
264
|
+
/** Length of the longest prefix that does not end inside a UTF-8 character. */
|
|
265
|
+
function completeUtf8Length(bytes) {
|
|
266
|
+
let start = bytes.byteLength;
|
|
267
|
+
while (start > 0 && ((bytes[start - 1] ?? 0) & 0xc0) === 0x80)
|
|
268
|
+
start -= 1;
|
|
269
|
+
if (start === 0)
|
|
270
|
+
return 0;
|
|
271
|
+
const lead = bytes[start - 1] ?? 0;
|
|
272
|
+
const width = lead >= 0xf0 ? 4 : lead >= 0xe0 ? 3 : lead >= 0xc0 ? 2 : 1;
|
|
273
|
+
return start - 1 + width <= bytes.byteLength ? bytes.byteLength : start - 1;
|
|
274
|
+
}
|
|
275
|
+
function kibibytes(bytes) {
|
|
276
|
+
return `${(bytes / 1_024).toFixed(1)} KiB`;
|
|
277
|
+
}
|
|
278
|
+
function textOutput(captured, stream) {
|
|
271
279
|
const bytes = new Uint8Array(captured.byteLength);
|
|
272
280
|
let offset = 0;
|
|
273
281
|
for (const chunk of captured.chunks) {
|
|
274
282
|
bytes.set(chunk, offset);
|
|
275
283
|
offset += chunk.byteLength;
|
|
276
284
|
}
|
|
285
|
+
if (captured.totalBytes <= captured.byteLength) {
|
|
286
|
+
return { text: new TextDecoder().decode(bytes), truncation: [] };
|
|
287
|
+
}
|
|
288
|
+
const keptBytes = completeUtf8Length(bytes);
|
|
289
|
+
const text = new TextDecoder().decode(bytes.subarray(0, keptBytes));
|
|
277
290
|
return {
|
|
278
|
-
text:
|
|
279
|
-
|
|
291
|
+
text: `${text}\n[fungi: ${stream} truncated: ${kibibytes(keptBytes)} of ${kibibytes(captured.totalBytes)} shown; redirect to a file or use head/tail]`,
|
|
292
|
+
truncation: [
|
|
293
|
+
{
|
|
294
|
+
stream,
|
|
295
|
+
keptBytes,
|
|
296
|
+
totalBytes: captured.totalBytes,
|
|
297
|
+
},
|
|
298
|
+
],
|
|
280
299
|
};
|
|
281
300
|
}
|
|
301
|
+
function truncationReport(...parts) {
|
|
302
|
+
const all = parts.flat();
|
|
303
|
+
return all.length === 0 ? false : all;
|
|
304
|
+
}
|
|
282
305
|
function commandResult(exitCode, stdout, stderr) {
|
|
283
|
-
const standardOutput = textOutput(stdout);
|
|
284
|
-
const standardError = textOutput(stderr);
|
|
306
|
+
const standardOutput = textOutput(stdout, "stdout");
|
|
307
|
+
const standardError = textOutput(stderr, "stderr");
|
|
285
308
|
return {
|
|
286
309
|
status: exitCode === 0 ? "completed" : "failed",
|
|
287
310
|
exitCode,
|
|
288
311
|
stdout: standardOutput.text,
|
|
289
312
|
stderr: standardError.text,
|
|
290
|
-
truncated: standardOutput.
|
|
313
|
+
truncated: truncationReport(standardOutput.truncation, standardError.truncation),
|
|
291
314
|
};
|
|
292
315
|
}
|
|
293
316
|
/**
|
|
@@ -308,8 +331,12 @@ function describeRunRequest(request) {
|
|
|
308
331
|
* passes `executable`/`args` straight to `ChildProcess.make`, never through a
|
|
309
332
|
* shell; only the intentional shell form interpolates into `sh -c`.
|
|
310
333
|
*/
|
|
311
|
-
function buildRunCommand(request, cwd, executionPath) {
|
|
312
|
-
const env =
|
|
334
|
+
function buildRunCommand(request, cwd, executionPath, home, sandbox) {
|
|
335
|
+
const env = privateLocalExecEnvironment(executionPath, {
|
|
336
|
+
HOME: home,
|
|
337
|
+
XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR,
|
|
338
|
+
DBUS_SESSION_BUS_ADDRESS: process.env.DBUS_SESSION_BUS_ADDRESS,
|
|
339
|
+
});
|
|
313
340
|
// Effect's own default stdin is an open "pipe" that nothing closes, so an
|
|
314
341
|
// executable that reads stdin (`cat`, `python3 -`) would otherwise hang
|
|
315
342
|
// for the full timeout. "ignore" gives it immediate EOF, matching the
|
|
@@ -317,19 +344,27 @@ function buildRunCommand(request, cwd, executionPath) {
|
|
|
317
344
|
const stdin = request.stdin === undefined
|
|
318
345
|
? "ignore"
|
|
319
346
|
: Stream.make(new TextEncoder().encode(request.stdin));
|
|
320
|
-
|
|
321
|
-
?
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
}
|
|
347
|
+
const argv = "command" in request
|
|
348
|
+
? ["sh", "-c", request.command]
|
|
349
|
+
: [request.executable, ...request.args];
|
|
350
|
+
// Admission happens before spawn and is never retried after an executable
|
|
351
|
+
// may have run. Folder validity and requireSandbox belong to the host.
|
|
352
|
+
const launch = sandbox?.(cwd, argv);
|
|
353
|
+
if (launch !== undefined) {
|
|
354
|
+
delete env.XDG_RUNTIME_DIR;
|
|
355
|
+
delete env.DBUS_SESSION_BUS_ADDRESS;
|
|
356
|
+
}
|
|
357
|
+
return ChildProcess.make(launch?.executable ?? argv[0], launch?.args ?? argv.slice(1), {
|
|
358
|
+
cwd,
|
|
359
|
+
env: { ...env, ...launch?.environment },
|
|
360
|
+
extendEnv: false,
|
|
361
|
+
stdin,
|
|
362
|
+
additionalFds: launch?.seccompFilter === undefined
|
|
363
|
+
? undefined
|
|
364
|
+
: {
|
|
365
|
+
fd3: { type: "input", stream: Stream.make(launch.seccompFilter) },
|
|
366
|
+
},
|
|
367
|
+
});
|
|
333
368
|
}
|
|
334
369
|
/**
|
|
335
370
|
* Only the direct form names an executable the host itself must locate and
|
|
@@ -341,8 +376,8 @@ function isMissingExecutableFailure(cause) {
|
|
|
341
376
|
(cause.reason._tag === "NotFound" ||
|
|
342
377
|
cause.reason._tag === "PermissionDenied"));
|
|
343
378
|
}
|
|
344
|
-
async function runCommandAtInvocationBoundary(request, cwd, executionPath, signal) {
|
|
345
|
-
const processCommand = buildRunCommand(request, cwd, executionPath);
|
|
379
|
+
async function runCommandAtInvocationBoundary(request, cwd, executionPath, home, signal, sandbox) {
|
|
380
|
+
const processCommand = buildRunCommand(request, cwd, executionPath, home, sandbox);
|
|
346
381
|
const timeoutMs = Math.min(request.timeoutMs, maximumCommandTimeoutMs);
|
|
347
382
|
try {
|
|
348
383
|
return await runPrivateInvocationProcess(processCommand, signal, timeoutMs, {
|
|
@@ -390,8 +425,8 @@ export function runPrivateInvocationProcess(processCommand, signal, timeoutMs, d
|
|
|
390
425
|
if (timeoutMs === undefined)
|
|
391
426
|
throw new Error("Missing command deadline");
|
|
392
427
|
const elapsedMs = (yield* Clock.currentTimeMillis) - startedAt;
|
|
393
|
-
const standardOutput = textOutput(stdout);
|
|
394
|
-
const standardError = textOutput(stderr);
|
|
428
|
+
const standardOutput = textOutput(stdout, "stdout");
|
|
429
|
+
const standardError = textOutput(stderr, "stderr");
|
|
395
430
|
return {
|
|
396
431
|
status: "deadline_exceeded",
|
|
397
432
|
timeoutMs,
|
|
@@ -406,23 +441,27 @@ export function runPrivateInvocationProcess(processCommand, signal, timeoutMs, d
|
|
|
406
441
|
timeoutMs,
|
|
407
442
|
requestedMs: describe.requestedMs ?? timeoutMs,
|
|
408
443
|
}),
|
|
409
|
-
truncated: standardOutput.
|
|
444
|
+
truncated: truncationReport(standardOutput.truncation, standardError.truncation),
|
|
410
445
|
};
|
|
411
446
|
})).pipe(Effect.provide(NodeServices.layer)), { signal });
|
|
412
447
|
}
|
|
413
|
-
async function execute(root, executionPath, request, context) {
|
|
448
|
+
async function execute(root, executionPath, home, request, context, sandbox) {
|
|
414
449
|
context.signal.throwIfAborted();
|
|
415
450
|
const cwd = await resolveCwd(root, request.cwd);
|
|
416
451
|
context.signal.throwIfAborted();
|
|
417
|
-
return runCommandAtInvocationBoundary(request, cwd, executionPath, context.signal);
|
|
452
|
+
return runCommandAtInvocationBoundary(request, cwd, executionPath, home, context.signal, sandbox);
|
|
418
453
|
}
|
|
419
454
|
/**
|
|
420
455
|
* Construct one private invocation-root Computer: bounded shell exec, plus the
|
|
421
456
|
* background-process operations of `processes` when the host supplies a scope.
|
|
422
457
|
*/
|
|
423
|
-
export async function createPrivateInvocationComputer(invocationRoot, executionPath = capturePrivateLocalExecPath(process.env.PATH ?? "/usr/bin:/bin", process.cwd()), processes
|
|
424
|
-
|
|
458
|
+
export async function createPrivateInvocationComputer(invocationRoot, executionPath = capturePrivateLocalExecPath(process.env.PATH ?? "/usr/bin:/bin", process.cwd()), processes,
|
|
459
|
+
/** Selected host home; standalone callers capture it before any await. */
|
|
460
|
+
home = capturePrivateLocalConfigRoot().home,
|
|
461
|
+
/** Native Local always supplies its live per-Agent admission callback. */
|
|
462
|
+
sandbox) {
|
|
425
463
|
const path = parsePrivateLocalExecPath(executionPath);
|
|
464
|
+
const root = await realpath(resolve(invocationRoot));
|
|
426
465
|
return Object.freeze({
|
|
427
466
|
getCapabilities: () => Promise.resolve(computerCapabilities(root, processes)),
|
|
428
467
|
call: async (capabilityId, input, context) => {
|
|
@@ -450,9 +489,9 @@ export async function createPrivateInvocationComputer(invocationRoot, executionP
|
|
|
450
489
|
},
|
|
451
490
|
});
|
|
452
491
|
if (request.operation === "run")
|
|
453
|
-
return execute(root, path, request, context);
|
|
492
|
+
return execute(root, path, home, request, context, sandbox);
|
|
454
493
|
if (processes !== undefined)
|
|
455
|
-
return jsonValueSchema.parse(await callBackgroundExec(processes, request, (requested) => resolveCwd(root, requested), path, context.signal));
|
|
494
|
+
return jsonValueSchema.parse(await callBackgroundExec(processes, request, (requested) => resolveCwd(root, requested), path, context.signal, sandbox));
|
|
456
495
|
throw new Error(`Computer capability is unavailable: ${selected}`);
|
|
457
496
|
},
|
|
458
497
|
});
|
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
import { type FilesPort } from "@fungi.computer/knapsack";
|
|
2
|
+
import type { LocalSandboxLaunch } from "./private-invocation-computer.js";
|
|
3
|
+
/** Same live authority as exec; the policy module owns protected-path checks. */
|
|
4
|
+
export type LocalSandboxWrites = Readonly<{
|
|
5
|
+
assertWrite(path: string): void;
|
|
6
|
+
launch: LocalSandboxLaunch;
|
|
7
|
+
}>;
|
|
8
|
+
/**
|
|
9
|
+
* Hidden scan work for one glob or grep: directory entries admitted to the
|
|
10
|
+
* walk (gitignored and `.git` entries are never admitted) and file bytes read.
|
|
11
|
+
* The result limit counts returned results; these bound the work behind them.
|
|
12
|
+
*/
|
|
13
|
+
export type ScanBounds = Readonly<{
|
|
14
|
+
candidates: number;
|
|
15
|
+
bytes: number;
|
|
16
|
+
}>;
|
|
2
17
|
/** Safe Files path failures surfaced across the workerd binding boundary. */
|
|
3
18
|
export declare class FilesPathError extends Error {
|
|
4
19
|
readonly reason: "outside-root" | "not-found" | "not-file";
|
|
@@ -12,4 +27,4 @@ export declare class FilesPathError extends Error {
|
|
|
12
27
|
* in-root symlinks. This is trusted same-user containment, not protection from
|
|
13
28
|
* a same-user process racing pathname resolution.
|
|
14
29
|
*/
|
|
15
|
-
export declare function createPrivateInvocationFilesPort(path: string): Promise<FilesPort>;
|
|
30
|
+
export declare function createPrivateInvocationFilesPort(path: string, scanBounds?: ScanBounds, sandbox?: LocalSandboxWrites): Promise<FilesPort>;
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { lstat, open, opendir, realpath, stat } from "node:fs/promises";
|
|
2
2
|
import { isAbsolute, join, matchesGlob, normalize, relative, resolve, sep, } from "node:path";
|
|
3
3
|
import { FilesEditError } from "@fungi.computer/knapsack";
|
|
4
|
+
import { SearchVisibility } from "@fungi.computer/knapsack/files-search";
|
|
5
|
+
import { imageReadResult, imageSniffBytes, imageTooLargeMessage, sniffImageMimeType, } from "@fungi.computer/knapsack/files-read";
|
|
4
6
|
import { RE2JS } from "re2js";
|
|
5
7
|
import { createPrivateFilesPatch } from "./private-files-patch.js";
|
|
8
|
+
import { fitLocalImage, maximumLocalImageInputBytes, } from "./local-image-fit.js";
|
|
6
9
|
import writeFileAtomic from "write-file-atomic";
|
|
7
10
|
const maximumFileBytes = 128 * 1_024;
|
|
8
|
-
const
|
|
9
|
-
|
|
11
|
+
const defaultScanBounds = {
|
|
12
|
+
candidates: 50_000,
|
|
13
|
+
bytes: 64 * 1_024 * 1_024,
|
|
14
|
+
};
|
|
10
15
|
const utf8 = new TextEncoder();
|
|
11
16
|
/** Safe Files path failures surfaced across the workerd binding boundary. */
|
|
12
17
|
export class FilesPathError extends Error {
|
|
@@ -38,13 +43,17 @@ async function existingRealpath(candidate, requested) {
|
|
|
38
43
|
throw cause;
|
|
39
44
|
}
|
|
40
45
|
}
|
|
41
|
-
/** One
|
|
46
|
+
/** One explicit bound on walked entries and one on bytes read. */
|
|
42
47
|
class ScanBudget {
|
|
48
|
+
bounds;
|
|
43
49
|
bytes = 0;
|
|
44
50
|
candidates = 0;
|
|
45
51
|
exhausted = false;
|
|
52
|
+
constructor(bounds) {
|
|
53
|
+
this.bounds = bounds;
|
|
54
|
+
}
|
|
46
55
|
admitCandidate() {
|
|
47
|
-
if (this.candidates ===
|
|
56
|
+
if (this.candidates === this.bounds.candidates) {
|
|
48
57
|
this.exhausted = true;
|
|
49
58
|
return false;
|
|
50
59
|
}
|
|
@@ -52,7 +61,7 @@ class ScanBudget {
|
|
|
52
61
|
return true;
|
|
53
62
|
}
|
|
54
63
|
admitBytes(bytes) {
|
|
55
|
-
if (bytes >
|
|
64
|
+
if (bytes > this.bounds.bytes - this.bytes) {
|
|
56
65
|
this.exhausted = true;
|
|
57
66
|
return false;
|
|
58
67
|
}
|
|
@@ -191,6 +200,18 @@ class InvocationRoot {
|
|
|
191
200
|
throw new FilesPathError("outside-root", requested);
|
|
192
201
|
}
|
|
193
202
|
}
|
|
203
|
+
async function readBytesAt(file, path, position, length, context) {
|
|
204
|
+
const buffer = Buffer.alloc(Math.min(length, file.size - position));
|
|
205
|
+
let bytesRead = 0;
|
|
206
|
+
while (bytesRead < buffer.byteLength) {
|
|
207
|
+
checkCancellation(context);
|
|
208
|
+
const next = await file.handle.read(buffer, bytesRead, buffer.byteLength - bytesRead, position + bytesRead);
|
|
209
|
+
if (next.bytesRead === 0)
|
|
210
|
+
throw new Error(`Files path changed during read: ${path.requested}`);
|
|
211
|
+
bytesRead += next.bytesRead;
|
|
212
|
+
}
|
|
213
|
+
return buffer;
|
|
214
|
+
}
|
|
194
215
|
async function readPage(path, offset, limit, context) {
|
|
195
216
|
const file = await openRegularFile(path, context);
|
|
196
217
|
try {
|
|
@@ -205,16 +226,19 @@ async function readPage(path, offset, limit, context) {
|
|
|
205
226
|
truncated: false,
|
|
206
227
|
};
|
|
207
228
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
229
|
+
if (offset === 0) {
|
|
230
|
+
const head = await readBytesAt(file, path, 0, imageSniffBytes, context);
|
|
231
|
+
const mimeType = sniffImageMimeType(head);
|
|
232
|
+
if (mimeType !== undefined) {
|
|
233
|
+
if (file.size > maximumLocalImageInputBytes)
|
|
234
|
+
throw new Error(imageTooLargeMessage(path.requested, file.size, maximumLocalImageInputBytes));
|
|
235
|
+
// Every image is shrunk here, before it becomes an image part.
|
|
236
|
+
return imageReadResult(path.requested, "image/jpeg", await fitLocalImage(path.requested, await readBytesAt(file, path, 0, file.size, context), context.signal));
|
|
237
|
+
}
|
|
217
238
|
}
|
|
239
|
+
const requestedBytes = Math.min(limit, file.size - offset);
|
|
240
|
+
const buffer = await readBytesAt(file, path, offset, requestedBytes, context);
|
|
241
|
+
const bytesRead = buffer.byteLength;
|
|
218
242
|
const page = decodeUtf8(buffer, path.requested, offset + bytesRead < file.size);
|
|
219
243
|
const nextOffset = offset + page.bytes;
|
|
220
244
|
return {
|
|
@@ -265,7 +289,34 @@ async function readGrepCandidate(path, context) {
|
|
|
265
289
|
return undefined;
|
|
266
290
|
}
|
|
267
291
|
}
|
|
268
|
-
|
|
292
|
+
function slashed(path) {
|
|
293
|
+
return path.split(sep).join("/");
|
|
294
|
+
}
|
|
295
|
+
async function readGitignore(root, directory, context) {
|
|
296
|
+
const candidate = join(directory, ".gitignore");
|
|
297
|
+
try {
|
|
298
|
+
await lstat(candidate);
|
|
299
|
+
}
|
|
300
|
+
catch (cause) {
|
|
301
|
+
if (missingPath(cause))
|
|
302
|
+
return undefined;
|
|
303
|
+
throw cause;
|
|
304
|
+
}
|
|
305
|
+
let resolved;
|
|
306
|
+
try {
|
|
307
|
+
resolved = await root.walked(candidate, candidate, context);
|
|
308
|
+
}
|
|
309
|
+
catch (cause) {
|
|
310
|
+
if (cause instanceof FilesPathError)
|
|
311
|
+
return undefined;
|
|
312
|
+
throw cause;
|
|
313
|
+
}
|
|
314
|
+
return await readGrepCandidate({ requested: candidate, resolved }, context);
|
|
315
|
+
}
|
|
316
|
+
function visiblePath(root, visibility, path, isDirectory) {
|
|
317
|
+
return visibility.visible(slashed(relative(root.canonicalPath, path)), isDirectory);
|
|
318
|
+
}
|
|
319
|
+
async function* walkFiles(root, base, budget, visibility, context) {
|
|
269
320
|
async function* visit(directory, relativeParent, ancestors) {
|
|
270
321
|
if (ancestors.has(directory))
|
|
271
322
|
return;
|
|
@@ -274,10 +325,12 @@ async function* walkFiles(root, base, budget, context) {
|
|
|
274
325
|
const handle = await opendir(directory);
|
|
275
326
|
for await (const entry of handle) {
|
|
276
327
|
checkCancellation(context);
|
|
328
|
+
const candidate = join(directory, entry.name);
|
|
329
|
+
if (!(await visiblePath(root, visibility, candidate, entry.isDirectory())))
|
|
330
|
+
continue;
|
|
277
331
|
if (!budget.admitCandidate())
|
|
278
332
|
return;
|
|
279
333
|
const relativePath = relativeParent === "" ? entry.name : join(relativeParent, entry.name);
|
|
280
|
-
const candidate = join(directory, entry.name);
|
|
281
334
|
const resolvedPath = await root.walked(candidate, outputPath(base.requested, relativePath), context);
|
|
282
335
|
checkCancellation(context);
|
|
283
336
|
const metadata = await stat(resolvedPath);
|
|
@@ -298,58 +351,104 @@ async function* walkFiles(root, base, budget, context) {
|
|
|
298
351
|
}
|
|
299
352
|
yield* visit(base.resolved, "", new Set());
|
|
300
353
|
}
|
|
301
|
-
async function grepFile(target, input,
|
|
354
|
+
async function grepFile(target, input, context) {
|
|
302
355
|
const matches = [];
|
|
303
356
|
const content = await readBoundedText(target, context);
|
|
357
|
+
const limited = appendMatchingLines(matches, input.path, content, lineMatcher(input.pattern, input.regex, input.ignoreCase), input.limit, context);
|
|
304
358
|
return {
|
|
305
359
|
path: input.path,
|
|
306
|
-
matches,
|
|
307
|
-
truncated:
|
|
360
|
+
results: matches,
|
|
361
|
+
truncated: limited ? "limit" : null,
|
|
308
362
|
};
|
|
309
363
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
for await (const entry of
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
if (!entry.isFile) {
|
|
318
|
-
incomplete = true;
|
|
364
|
+
/** One directory level; the entries past the limit are only witnessed, not returned. */
|
|
365
|
+
async function listLevel(root, directory, input, visibility, context) {
|
|
366
|
+
const handle = await opendir(directory.resolved);
|
|
367
|
+
const results = [];
|
|
368
|
+
for await (const entry of handle) {
|
|
369
|
+
checkCancellation(context);
|
|
370
|
+
if (!(await visiblePath(root, visibility, join(directory.resolved, entry.name), entry.isDirectory())))
|
|
319
371
|
continue;
|
|
372
|
+
if (results.length === input.limit) {
|
|
373
|
+
return { path: input.path, results, truncated: "limit" };
|
|
320
374
|
}
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
375
|
+
const metadata = await lstat(join(directory.resolved, entry.name));
|
|
376
|
+
const path = outputPath(input.path, entry.name);
|
|
377
|
+
results.push({ path, type: entryType(metadata, path) });
|
|
378
|
+
}
|
|
379
|
+
return { path: input.path, results, truncated: null };
|
|
380
|
+
}
|
|
381
|
+
/** One bounded walk for a glob or content search with an optional include glob. */
|
|
382
|
+
async function searchTree(root, directory, search, bounds, visibility, context) {
|
|
383
|
+
const { input } = search;
|
|
384
|
+
const pathPattern = search.kind === "glob" ? search.input.pattern : search.input.include;
|
|
385
|
+
const matchesLine = search.kind === "grep"
|
|
386
|
+
? lineMatcher(search.input.pattern, search.input.regex, search.input.ignoreCase)
|
|
387
|
+
: undefined;
|
|
388
|
+
const results = [];
|
|
389
|
+
const budget = new ScanBudget(bounds);
|
|
390
|
+
for await (const entry of walkFiles(root, directory, budget, visibility, context)) {
|
|
391
|
+
checkCancellation(context);
|
|
392
|
+
if (pathPattern !== undefined &&
|
|
393
|
+
!matchesGlob(entry.relativePath, pathPattern))
|
|
394
|
+
continue;
|
|
395
|
+
const path = outputPath(input.path, entry.relativePath);
|
|
396
|
+
if (matchesLine === undefined) {
|
|
397
|
+
if (results.length === input.limit) {
|
|
398
|
+
return { path: input.path, results, truncated: "limit" };
|
|
399
|
+
}
|
|
400
|
+
results.push({
|
|
401
|
+
path,
|
|
402
|
+
type: entry.isDirectory ? "directory" : "file",
|
|
403
|
+
});
|
|
324
404
|
continue;
|
|
325
405
|
}
|
|
406
|
+
// Directories, special files, oversized files and non-text files hold no
|
|
407
|
+
// searchable lines; skipping them is not a cut of the scan.
|
|
408
|
+
if (!entry.isFile || entry.size > maximumFileBytes)
|
|
409
|
+
continue;
|
|
326
410
|
if (!budget.admitBytes(entry.size))
|
|
327
411
|
break;
|
|
328
|
-
const content = await readGrepCandidate({ requested:
|
|
329
|
-
if (content === undefined)
|
|
330
|
-
incomplete = true;
|
|
412
|
+
const content = await readGrepCandidate({ requested: path, resolved: entry.resolvedPath }, context);
|
|
413
|
+
if (content === undefined)
|
|
331
414
|
continue;
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
return { path: input.path, matches, truncated: true };
|
|
415
|
+
if (appendMatchingLines(results, path, content, matchesLine, input.limit, context)) {
|
|
416
|
+
return { path: input.path, results, truncated: "limit" };
|
|
335
417
|
}
|
|
336
418
|
}
|
|
337
419
|
return {
|
|
338
420
|
path: input.path,
|
|
339
|
-
|
|
340
|
-
truncated:
|
|
421
|
+
results,
|
|
422
|
+
truncated: budget.exhausted ? "scan" : null,
|
|
341
423
|
};
|
|
342
424
|
}
|
|
343
|
-
async function
|
|
425
|
+
async function searchPath(root, search, bounds, context) {
|
|
426
|
+
const { input } = search;
|
|
344
427
|
const target = await root.existing(input.path, context);
|
|
345
428
|
checkCancellation(context);
|
|
346
|
-
const
|
|
347
|
-
const
|
|
348
|
-
if (
|
|
349
|
-
return
|
|
350
|
-
if (
|
|
351
|
-
|
|
352
|
-
|
|
429
|
+
const metadata = await stat(target.resolved);
|
|
430
|
+
const visibility = new SearchVisibility((directory) => readGitignore(root, join(root.canonicalPath, directory), context));
|
|
431
|
+
if (!(await visiblePath(root, visibility, target.resolved, metadata.isDirectory())))
|
|
432
|
+
return { path: input.path, results: [], truncated: null };
|
|
433
|
+
if (metadata.isFile()) {
|
|
434
|
+
if (search.kind === "glob")
|
|
435
|
+
throw new Error(`Files glob needs a directory path, not a file: ${input.path}`);
|
|
436
|
+
if (search.kind === "grep") {
|
|
437
|
+
if (search.input.include !== undefined)
|
|
438
|
+
throw new Error(`Files grep include needs a directory path, not a file: ${input.path}`);
|
|
439
|
+
return await grepFile(target, search.input, context);
|
|
440
|
+
}
|
|
441
|
+
return {
|
|
442
|
+
path: input.path,
|
|
443
|
+
results: [{ path: input.path, type: "file" }],
|
|
444
|
+
truncated: null,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
if (!metadata.isDirectory())
|
|
448
|
+
throw new Error(`Files path is not a file or directory: ${input.path}`);
|
|
449
|
+
if (search.kind === "list")
|
|
450
|
+
return await listLevel(root, target, input, visibility, context);
|
|
451
|
+
return await searchTree(root, target, search, bounds, visibility, context);
|
|
353
452
|
}
|
|
354
453
|
function createWriteQueue() {
|
|
355
454
|
let tail = Promise.resolve();
|
|
@@ -373,7 +472,7 @@ function createWriteQueue() {
|
|
|
373
472
|
* in-root symlinks. This is trusted same-user containment, not protection from
|
|
374
473
|
* a same-user process racing pathname resolution.
|
|
375
474
|
*/
|
|
376
|
-
export async function createPrivateInvocationFilesPort(path) {
|
|
475
|
+
export async function createPrivateInvocationFilesPort(path, scanBounds = defaultScanBounds, sandbox) {
|
|
377
476
|
const root = await InvocationRoot.capture(path);
|
|
378
477
|
const write = createWriteQueue();
|
|
379
478
|
const port = {
|
|
@@ -381,44 +480,13 @@ export async function createPrivateInvocationFilesPort(path) {
|
|
|
381
480
|
return await readPage(await root.existing(input.path, context), input.offset, input.limit, context);
|
|
382
481
|
},
|
|
383
482
|
async list(input, context) {
|
|
384
|
-
|
|
385
|
-
checkCancellation(context);
|
|
386
|
-
const handle = await opendir(directory.resolved);
|
|
387
|
-
const entries = [];
|
|
388
|
-
for await (const entry of handle) {
|
|
389
|
-
checkCancellation(context);
|
|
390
|
-
if (entries.length === input.limit) {
|
|
391
|
-
return { path: input.path, entries, truncated: true };
|
|
392
|
-
}
|
|
393
|
-
const metadata = await lstat(join(directory.resolved, entry.name));
|
|
394
|
-
const path = outputPath(input.path, entry.name);
|
|
395
|
-
entries.push({
|
|
396
|
-
path,
|
|
397
|
-
name: entry.name,
|
|
398
|
-
type: entryType(metadata, path),
|
|
399
|
-
size: metadata.size,
|
|
400
|
-
modifiedAt: metadata.mtimeMs,
|
|
401
|
-
});
|
|
402
|
-
}
|
|
403
|
-
return { path: input.path, entries, truncated: false };
|
|
483
|
+
return await searchPath(root, { kind: "list", input }, scanBounds, context);
|
|
404
484
|
},
|
|
405
485
|
async glob(input, context) {
|
|
406
|
-
|
|
407
|
-
const matches = [];
|
|
408
|
-
const budget = new ScanBudget();
|
|
409
|
-
for await (const entry of walkFiles(root, directory, budget, context)) {
|
|
410
|
-
checkCancellation(context);
|
|
411
|
-
if (!matchesGlob(entry.relativePath, input.pattern))
|
|
412
|
-
continue;
|
|
413
|
-
if (matches.length === input.limit) {
|
|
414
|
-
return { path: input.path, matches, truncated: true };
|
|
415
|
-
}
|
|
416
|
-
matches.push(outputPath(input.path, entry.relativePath));
|
|
417
|
-
}
|
|
418
|
-
return { path: input.path, matches, truncated: budget.exhausted };
|
|
486
|
+
return await searchPath(root, { kind: "glob", input }, scanBounds, context);
|
|
419
487
|
},
|
|
420
488
|
async grep(input, context) {
|
|
421
|
-
return await
|
|
489
|
+
return await searchPath(root, { kind: "grep", input }, scanBounds, context);
|
|
422
490
|
},
|
|
423
491
|
async replace(input, context) {
|
|
424
492
|
return await write(async () => {
|
|
@@ -432,6 +500,7 @@ export async function createPrivateInvocationFilesPort(path) {
|
|
|
432
500
|
throw new FilesEditError("file-too-large", input.path, `Result exceeds ${String(maximumFileBytes)} bytes; use files.patch for a small change`);
|
|
433
501
|
}
|
|
434
502
|
checkCancellation(context);
|
|
503
|
+
sandbox?.assertWrite(path.resolved);
|
|
435
504
|
await writeFileAtomic(path.resolved, input.content, {
|
|
436
505
|
encoding: "utf8",
|
|
437
506
|
});
|
|
@@ -439,7 +508,9 @@ export async function createPrivateInvocationFilesPort(path) {
|
|
|
439
508
|
});
|
|
440
509
|
},
|
|
441
510
|
};
|
|
442
|
-
|
|
511
|
+
// The native parser/patch engine still owns patch grammar. Its adapter must
|
|
512
|
+
// accept the launch callback too; never parse patch headers here.
|
|
513
|
+
const patch = await createPrivateFilesPatch(root.canonicalPath, port.read, sandbox?.launch);
|
|
443
514
|
if (patch === undefined)
|
|
444
515
|
return Object.freeze(port);
|
|
445
516
|
return Object.freeze({
|