@fungi.computer/truffle 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/private-files-patch.js +39 -36
- package/dist/private-invocation-computer.d.ts +224 -5
- package/dist/private-invocation-computer.js +162 -22
- package/dist/private-invocation-files.d.ts +3 -2
- package/dist/private-invocation-files.js +22 -3
- package/dist/private-local-agent-control.d.ts +2 -2
- package/dist/private-local-bwrap.d.ts +17 -7
- package/dist/private-local-bwrap.js +73 -33
- package/dist/private-local-computer-processes.d.ts +15 -0
- package/dist/private-local-computer-processes.js +13 -0
- package/dist/private-local-containment.d.ts +18 -8
- package/dist/private-local-containment.js +70 -45
- package/dist/private-local-delegation.d.ts +3 -3
- package/dist/private-local-delegation.js +32 -19
- package/dist/private-local-exec-review.d.ts +10 -0
- package/dist/private-local-exec-review.js +50 -0
- package/dist/private-local-exec-rules.d.ts +14 -0
- package/dist/private-local-exec-rules.js +420 -0
- package/dist/private-local-git-exclude.js +25 -15
- package/dist/private-local-linux-sandbox.d.ts +5 -3
- package/dist/private-local-linux-sandbox.js +71 -25
- package/dist/private-local-mail.d.ts +2 -2
- package/dist/private-local-mesh-links.d.ts +4 -1
- package/dist/private-local-mesh-links.js +50 -3
- package/dist/private-local-native-digests.js +2 -0
- package/dist/private-local-native-install-error.d.ts +2 -0
- package/dist/private-local-native-install-error.js +10 -0
- package/dist/private-local-native-staging.d.ts +16 -0
- package/dist/private-local-native-staging.js +138 -0
- package/dist/private-local-permission-mode.d.ts +10 -0
- package/dist/private-local-permission-mode.js +18 -0
- package/dist/private-local-runtime-host.d.ts +19 -5
- package/dist/private-local-runtime-host.js +391 -71
- package/dist/private-local-sandbox-policy.d.ts +46 -6
- package/dist/private-local-sandbox-policy.js +350 -71
- package/dist/private-local-sandbox.d.ts +34 -18
- package/dist/private-local-sandbox.js +225 -60
- package/dist/private-local-seatbelt.d.ts +7 -13
- package/dist/private-local-seatbelt.js +55 -32
- package/dist/private-local-service-contract.d.ts +5 -4
- package/dist/private-local-service-discovery.d.ts +18 -6
- package/dist/private-local-service-discovery.js +4 -3
- package/dist/private-local-service-listener.js +40 -2
- package/dist/private-local-service-log.d.ts +8 -0
- package/dist/private-local-service-log.js +3 -0
- package/dist/private-local-service-owner.d.ts +18 -10
- package/dist/private-local-service-owner.js +117 -25
- package/dist/private-local-service.js +43 -9
- package/dist/private-local-session-discovery.d.ts +3 -2
- package/dist/private-local-session-discovery.js +18 -6
- package/dist/private-mcp-form-contract.d.ts +8 -1
- package/dist/private-mcp-form-contract.js +8 -1
- package/dist/private-mcp-form-session.d.ts +33 -3
- package/dist/private-mcp-form-session.js +28 -5
- package/dist/private-oyster-mcp-auth.d.ts +1 -1
- package/dist/private-oyster-mcp-auth.js +139 -7
- package/dist/private-provider-rejection.js +1 -1
- package/dist/seatbelt/safe-base.sbpl +2 -2
- package/native/bin/bwrap-build-source/Dockerfile +3 -1
- package/native/bin/bwrap-build-source/README.md +6 -6
- package/native/bin/bwrap-build-source/pin.mjs +2 -2
- package/native/bin/bwrap-build-source/record.mjs +12 -1
- package/native/bin/bwrap-build-source/vendor/seccomp-unix-block.c +10 -7
- package/native/bin/bwrap-build.json +3 -2
- package/native/bin/fungi-apply-patch +0 -0
- package/native/bin/fungi-seccomp-workspace.bpf +0 -0
- package/native/bin/fungi-seccomp-workspace.bpf.sha256 +1 -0
- package/package.json +25 -19
|
@@ -7,6 +7,15 @@ import { RE2JS } from "re2js";
|
|
|
7
7
|
import { createPrivateFilesPatch } from "./private-files-patch.js";
|
|
8
8
|
import { fitLocalImage, maximumLocalImageInputBytes, } from "./local-image-fit.js";
|
|
9
9
|
import writeFileAtomic from "write-file-atomic";
|
|
10
|
+
const readOnlyFilesMessage = "Read-only mode: switch to Workspace to edit";
|
|
11
|
+
/** An expected Files refusal, not a proposal for an approval. */
|
|
12
|
+
class ReadOnlyFilesRefusal extends FilesEditError {
|
|
13
|
+
constructor(path) {
|
|
14
|
+
super("read-only", path, readOnlyFilesMessage);
|
|
15
|
+
this.name = "ReadOnlyFilesRefusal";
|
|
16
|
+
this.message = readOnlyFilesMessage;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
10
19
|
const maximumFileBytes = 128 * 1_024;
|
|
11
20
|
const defaultScanBounds = {
|
|
12
21
|
candidates: 50_000,
|
|
@@ -472,10 +481,14 @@ function createWriteQueue() {
|
|
|
472
481
|
* in-root symlinks. This is trusted same-user containment, not protection from
|
|
473
482
|
* a same-user process racing pathname resolution.
|
|
474
483
|
*/
|
|
475
|
-
export async function createPrivateInvocationFilesPort(path, scanBounds = defaultScanBounds, sandbox) {
|
|
484
|
+
export async function createPrivateInvocationFilesPort(path, scanBounds = defaultScanBounds, sandbox, mode = () => "full-access") {
|
|
476
485
|
const root = await InvocationRoot.capture(path);
|
|
477
486
|
const write = createWriteQueue();
|
|
478
487
|
const port = {
|
|
488
|
+
assertEditable(requested) {
|
|
489
|
+
if (mode() === "read-only")
|
|
490
|
+
throw new ReadOnlyFilesRefusal(requested);
|
|
491
|
+
},
|
|
479
492
|
async read(input, context) {
|
|
480
493
|
return await readPage(await root.existing(input.path, context), input.offset, input.limit, context);
|
|
481
494
|
},
|
|
@@ -491,6 +504,7 @@ export async function createPrivateInvocationFilesPort(path, scanBounds = defaul
|
|
|
491
504
|
async replace(input, context) {
|
|
492
505
|
return await write(async () => {
|
|
493
506
|
checkCancellation(context);
|
|
507
|
+
port.assertEditable?.(input.path);
|
|
494
508
|
const path = await root.existing(input.path, context);
|
|
495
509
|
const current = await readBoundedText(path, context);
|
|
496
510
|
if (current !== input.expectedContent)
|
|
@@ -500,7 +514,7 @@ export async function createPrivateInvocationFilesPort(path, scanBounds = defaul
|
|
|
500
514
|
throw new FilesEditError("file-too-large", input.path, `Result exceeds ${String(maximumFileBytes)} bytes; use files.patch for a small change`);
|
|
501
515
|
}
|
|
502
516
|
checkCancellation(context);
|
|
503
|
-
sandbox?.assertWrite(path.resolved);
|
|
517
|
+
sandbox?.assertWrite([path.resolved]);
|
|
504
518
|
await writeFileAtomic(path.resolved, input.content, {
|
|
505
519
|
encoding: "utf8",
|
|
506
520
|
});
|
|
@@ -515,6 +529,11 @@ export async function createPrivateInvocationFilesPort(path, scanBounds = defaul
|
|
|
515
529
|
return Object.freeze(port);
|
|
516
530
|
return Object.freeze({
|
|
517
531
|
...port,
|
|
518
|
-
patch: (input, context) => write(() =>
|
|
532
|
+
patch: (input, context) => write(async () => {
|
|
533
|
+
checkCancellation(context);
|
|
534
|
+
if (mode() === "read-only")
|
|
535
|
+
throw new ReadOnlyFilesRefusal(root.canonicalPath);
|
|
536
|
+
return patch(input, context);
|
|
537
|
+
}),
|
|
519
538
|
});
|
|
520
539
|
}
|
|
@@ -70,7 +70,7 @@ export declare function createPrivateLocalAgentControl(input: Readonly<{
|
|
|
70
70
|
list: (signal: AbortSignal, options?: Readonly<{
|
|
71
71
|
archived?: boolean;
|
|
72
72
|
}>) => Promise<readonly AgentControlAgent[]>;
|
|
73
|
-
send?: (input: import("@fungi.computer/shiitake/agent-control").AgentDelegationInput, sourceSessionId: string, signal: AbortSignal, invocation?: import("@fungi.computer/shiitake/agent-control").AgentDelegationInvocation) => Promise<import("@fungi.computer/shiitake").Accepted>;
|
|
73
|
+
send?: (input: import("@fungi.computer/shiitake/agent-control").AgentDelegationInput, sourceSessionId: string, signal: AbortSignal, invocation?: import("@fungi.computer/shiitake/agent-control").AgentDelegationInvocation) => Promise<import("@fungi.computer/shiitake").Accepted | import("@fungi.computer/shiitake").Held>;
|
|
74
74
|
recall?: (input: import("@fungi.computer/shiitake/agent-control").AgentRecallInput, sourceSessionId: string, signal: AbortSignal) => Promise<import("@fungi.computer/shiitake").CancelResult>;
|
|
75
75
|
create?: (input: Readonly<{
|
|
76
76
|
requestId: string;
|
|
@@ -91,7 +91,7 @@ export declare function createPrivateLocalAgentControl(input: Readonly<{
|
|
|
91
91
|
models?: (signal: AbortSignal) => Promise<readonly import("@fungi.computer/shiitake/agent-control").AgentControlModel[]>;
|
|
92
92
|
checkModel?: (input: Readonly<{
|
|
93
93
|
agentId: string;
|
|
94
|
-
sessionId
|
|
94
|
+
sessionId?: string;
|
|
95
95
|
}>, signal: AbortSignal) => Promise<void>;
|
|
96
96
|
open: (agentId: string, signal: AbortSignal) => Promise<Readonly<{
|
|
97
97
|
agent: import("@fungi.computer/shiitake").ShiitakeAgent;
|
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
export declare class LocalBwrapUnavailable extends Error {
|
|
2
|
-
readonly reason: "
|
|
2
|
+
readonly reason: "install";
|
|
3
3
|
readonly name = "LocalBwrapUnavailable";
|
|
4
|
-
constructor(reason: "
|
|
4
|
+
constructor(reason: "install", message: string, options?: ErrorOptions);
|
|
5
5
|
}
|
|
6
6
|
export type LinuxSandboxPayload = {
|
|
7
|
-
readonly
|
|
8
|
-
readonly
|
|
7
|
+
readonly sourcePath: string;
|
|
8
|
+
readonly executableBytes: Uint8Array;
|
|
9
|
+
readonly seccomp: Uint8Array;
|
|
10
|
+
readonly seccompWorkspace: Uint8Array;
|
|
9
11
|
};
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
export type LinuxSandboxDigests = Readonly<{
|
|
13
|
+
bwrap: string;
|
|
14
|
+
seccomp: string;
|
|
15
|
+
seccompWorkspace: string;
|
|
16
|
+
}>;
|
|
17
|
+
/** Packaged patch has no sidecar; the built module owns its digest. */
|
|
18
|
+
export type NativeSourceReader = (path: string) => Buffer;
|
|
19
|
+
export declare function verifiedNativePatchHelper(path: string, project: string, readNativeSource?: NativeSourceReader): Uint8Array;
|
|
20
|
+
/** Verify the three packaged Linux files once before copying into service-owned staging. */
|
|
21
|
+
export declare function verifyLocalLinuxSandboxPayload(executable: string, project: string, expected: LinuxSandboxDigests, readNativeSource?: NativeSourceReader): LinuxSandboxPayload;
|
|
12
22
|
/** Same optional native-package lookup as apply_patch, with no PATH fallback. */
|
|
13
|
-
export declare function localLinuxSandboxPayload(): LinuxSandboxPayload;
|
|
23
|
+
export declare function localLinuxSandboxPayload(project: string, readNativeSource?: NativeSourceReader): LinuxSandboxPayload;
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { lstatSync, readFileSync, statSync } from "node:fs";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
.string()
|
|
9
|
-
.trim()
|
|
10
|
-
.regex(/^[a-f0-9]{64}$/u);
|
|
6
|
+
import { nativeDigests } from "./private-local-native-digests.js";
|
|
7
|
+
import { assertHostOwnedNativeFile, UnsafeNativePayload, } from "./private-local-sandbox-policy.js";
|
|
11
8
|
export class LocalBwrapUnavailable extends Error {
|
|
12
9
|
reason;
|
|
13
10
|
name = "LocalBwrapUnavailable";
|
|
@@ -16,55 +13,98 @@ export class LocalBwrapUnavailable extends Error {
|
|
|
16
13
|
this.reason = reason;
|
|
17
14
|
}
|
|
18
15
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
16
|
+
export function verifiedNativePatchHelper(path, project, readNativeSource = readFileSync) {
|
|
17
|
+
const expected = nativeDigests[`${process.platform}-${process.arch}`]?.patch;
|
|
18
|
+
if (expected === undefined)
|
|
19
|
+
throw new LocalBwrapUnavailable("install", "No built native patch digest for this platform; reinstall Fungi.");
|
|
20
|
+
return verifiedBytes(path, "fungi-apply-patch", project, expected, readNativeSource);
|
|
21
|
+
}
|
|
22
|
+
/** Verify the three packaged Linux files once before copying into service-owned staging. */
|
|
23
|
+
export function verifyLocalLinuxSandboxPayload(executable, project, expected, readNativeSource = readFileSync) {
|
|
24
|
+
const executableBytes = verifiedBytes(executable, "fungi-bwrap", project, expected.bwrap, readNativeSource);
|
|
28
25
|
return {
|
|
29
|
-
executable,
|
|
30
|
-
|
|
26
|
+
sourcePath: executable,
|
|
27
|
+
executableBytes,
|
|
28
|
+
seccomp: verifiedBytes(join(dirname(executable), "fungi-seccomp.bpf"), "fungi-seccomp.bpf", project, expected.seccomp, readNativeSource),
|
|
29
|
+
seccompWorkspace: verifiedBytes(join(dirname(executable), "fungi-seccomp-workspace.bpf"), "fungi-seccomp-workspace.bpf", project, expected.seccompWorkspace, readNativeSource),
|
|
31
30
|
};
|
|
32
31
|
}
|
|
33
|
-
function verifiedBytes(path, label) {
|
|
32
|
+
function verifiedBytes(path, label, project, expected, readNativeSource = readFileSync) {
|
|
34
33
|
let bytes;
|
|
35
|
-
let digest;
|
|
36
34
|
try {
|
|
35
|
+
assertHostOwnedNativeFile(project, path);
|
|
37
36
|
if (!statSync(path).isFile())
|
|
38
|
-
throw new LocalBwrapUnavailable("
|
|
39
|
-
bytes =
|
|
40
|
-
digest = readFileSync(`${path}.sha256`, "utf8");
|
|
37
|
+
throw new LocalBwrapUnavailable("install", `Packaged ${label} is not a regular file; reinstall Fungi.`);
|
|
38
|
+
bytes = readNativeSource(path);
|
|
41
39
|
}
|
|
42
40
|
catch (cause) {
|
|
43
41
|
if (cause instanceof Error && "code" in cause && cause.code === "ENOENT")
|
|
44
|
-
throw new LocalBwrapUnavailable("
|
|
45
|
-
|
|
42
|
+
throw new LocalBwrapUnavailable("install", `Packaged ${label} is missing; reinstall Fungi with optional dependencies enabled.`, { cause });
|
|
43
|
+
if (cause instanceof UnsafeNativePayload ||
|
|
44
|
+
cause instanceof LocalBwrapUnavailable)
|
|
45
|
+
throw cause;
|
|
46
|
+
throw new LocalBwrapUnavailable("install", `Packaged ${label} cannot be read; reinstall Fungi. No command was started.`, { cause });
|
|
46
47
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
throw new LocalBwrapUnavailable("invalid-digest", `Packaged ${label} has an invalid SHA-256 record; reinstall Fungi.`);
|
|
50
|
-
if (createHash("sha256").update(bytes).digest("hex") !== expected.data)
|
|
51
|
-
throw new LocalBwrapUnavailable("digest-mismatch", `Packaged ${label} digest mismatch; reinstall Fungi. No command was started.`);
|
|
48
|
+
if (createHash("sha256").update(bytes).digest("hex") !== expected)
|
|
49
|
+
throw new LocalBwrapUnavailable("install", `Packaged ${label} digest mismatch; reinstall Fungi. No command was started.`);
|
|
52
50
|
return bytes;
|
|
53
51
|
}
|
|
54
52
|
/** Same optional native-package lookup as apply_patch, with no PATH fallback. */
|
|
55
|
-
export function localLinuxSandboxPayload() {
|
|
53
|
+
export function localLinuxSandboxPayload(project, readNativeSource = readFileSync) {
|
|
54
|
+
const digest = nativeDigests[`${process.platform}-${process.arch}`];
|
|
55
|
+
if (digest?.bwrap === undefined ||
|
|
56
|
+
digest.seccomp === undefined ||
|
|
57
|
+
digest.seccompWorkspace === undefined)
|
|
58
|
+
throw new LocalBwrapUnavailable("install", "No built native Linux payload digest for this platform; reinstall Fungi.");
|
|
56
59
|
let executable;
|
|
57
60
|
if (process.env.FUNGI_BUNDLED_CLI) {
|
|
58
61
|
const name = `@fungi.computer/spore-native-${process.platform}-${process.arch}`;
|
|
62
|
+
const message = `Packaged ${name}/bin/fungi-bwrap cannot be read; reinstall Fungi.`;
|
|
63
|
+
let directories;
|
|
64
|
+
try {
|
|
65
|
+
directories = createRequire(process.env.FUNGI_BUNDLED_CLI).resolve.paths(name);
|
|
66
|
+
}
|
|
67
|
+
catch (cause) {
|
|
68
|
+
throw new LocalBwrapUnavailable("install", message, { cause });
|
|
69
|
+
}
|
|
70
|
+
if (directories === null)
|
|
71
|
+
throw new LocalBwrapUnavailable("install", message);
|
|
72
|
+
let packageRoot;
|
|
73
|
+
for (const directory of directories) {
|
|
74
|
+
const candidate = join(directory, name);
|
|
75
|
+
try {
|
|
76
|
+
if (lstatSync(candidate, { throwIfNoEntry: false }) !== undefined) {
|
|
77
|
+
packageRoot = candidate;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch (cause) {
|
|
82
|
+
throw new LocalBwrapUnavailable("install", message, { cause });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (packageRoot === undefined)
|
|
86
|
+
throw new LocalBwrapUnavailable("install", `Missing ${name}; reinstall Fungi with optional dependencies enabled.`);
|
|
87
|
+
// Package metadata (including exports) is not an authority for whether
|
|
88
|
+
// native bytes are absent. Check the package's location before its bin.
|
|
59
89
|
try {
|
|
60
|
-
|
|
90
|
+
assertHostOwnedNativeFile(project, packageRoot);
|
|
91
|
+
if (!statSync(packageRoot).isDirectory())
|
|
92
|
+
throw new LocalBwrapUnavailable("install", message);
|
|
61
93
|
}
|
|
62
94
|
catch (cause) {
|
|
63
|
-
|
|
95
|
+
if (cause instanceof UnsafeNativePayload ||
|
|
96
|
+
cause instanceof LocalBwrapUnavailable)
|
|
97
|
+
throw cause;
|
|
98
|
+
throw new LocalBwrapUnavailable("install", message, { cause });
|
|
64
99
|
}
|
|
100
|
+
executable = join(packageRoot, "bin", "fungi-bwrap");
|
|
65
101
|
}
|
|
66
102
|
else {
|
|
67
103
|
executable = fileURLToPath(new URL("../native/bin/fungi-bwrap", import.meta.url));
|
|
68
104
|
}
|
|
69
|
-
return verifyLocalLinuxSandboxPayload(executable
|
|
105
|
+
return verifyLocalLinuxSandboxPayload(executable, project, {
|
|
106
|
+
bwrap: digest.bwrap,
|
|
107
|
+
seccomp: digest.seccomp,
|
|
108
|
+
seccompWorkspace: digest.seccompWorkspace,
|
|
109
|
+
}, readNativeSource);
|
|
70
110
|
}
|
|
@@ -128,11 +128,17 @@ export declare function createPrivateLocalComputerProcesses(root: string, home?:
|
|
|
128
128
|
*/
|
|
129
129
|
export declare const backgroundExecInput: z.ZodUnion<readonly [z.ZodObject<{
|
|
130
130
|
operation: z.ZodLiteral<"start">;
|
|
131
|
+
escape: z.ZodOptional<z.ZodObject<{
|
|
132
|
+
justification: z.ZodString;
|
|
133
|
+
}, z.core.$strict>>;
|
|
131
134
|
command: z.ZodString;
|
|
132
135
|
stdin: z.ZodOptional<z.ZodString>;
|
|
133
136
|
cwd: z.ZodOptional<z.ZodString>;
|
|
134
137
|
}, z.core.$strict>, z.ZodObject<{
|
|
135
138
|
operation: z.ZodLiteral<"start">;
|
|
139
|
+
escape: z.ZodOptional<z.ZodObject<{
|
|
140
|
+
justification: z.ZodString;
|
|
141
|
+
}, z.core.$strict>>;
|
|
136
142
|
executable: z.ZodString;
|
|
137
143
|
args: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
138
144
|
stdin: z.ZodOptional<z.ZodString>;
|
|
@@ -178,6 +184,15 @@ export declare const backgroundExecOutput: z.ZodUnion<readonly [z.ZodObject<{
|
|
|
178
184
|
complete: z.ZodBoolean;
|
|
179
185
|
timedOut: z.ZodBoolean;
|
|
180
186
|
signalled: z.ZodOptional<z.ZodBoolean>;
|
|
187
|
+
sandbox: z.ZodOptional<z.ZodObject<{
|
|
188
|
+
mode: z.ZodEnum<{
|
|
189
|
+
"read-only": "read-only";
|
|
190
|
+
workspace: "workspace";
|
|
191
|
+
"full-access": "full-access";
|
|
192
|
+
}>;
|
|
193
|
+
likelyDenied: z.ZodBoolean;
|
|
194
|
+
ranOutsideSandbox: z.ZodBoolean;
|
|
195
|
+
}, z.core.$strict>>;
|
|
181
196
|
}, z.core.$strict>, z.ZodObject<{
|
|
182
197
|
processes: z.ZodArray<z.ZodObject<{
|
|
183
198
|
id: z.ZodString & z.ZodType<import("@fungi.computer/cairn").CairnId<"local-computer-process", "process", 1>, string, z.core.$ZodTypeInternals<import("@fungi.computer/cairn").CairnId<"local-computer-process", "process", 1>, string>>;
|
|
@@ -65,6 +65,9 @@ const processSchema = z.strictObject({
|
|
|
65
65
|
/** The intentional shell form of background `start`. */
|
|
66
66
|
const shellStartInput = z.strictObject({
|
|
67
67
|
operation: z.literal("start"),
|
|
68
|
+
escape: z
|
|
69
|
+
.strictObject({ justification: z.string().min(1).max(2048) })
|
|
70
|
+
.optional(),
|
|
68
71
|
command: z.string().min(1).max(maximumLaunchTextLength),
|
|
69
72
|
stdin: launchStdinSchema.optional(),
|
|
70
73
|
cwd: launchCwdSchema.optional(),
|
|
@@ -73,6 +76,9 @@ const shellStartInput = z.strictObject({
|
|
|
73
76
|
const directStartInput = z
|
|
74
77
|
.strictObject({
|
|
75
78
|
operation: z.literal("start"),
|
|
79
|
+
escape: z
|
|
80
|
+
.strictObject({ justification: z.string().min(1).max(2048) })
|
|
81
|
+
.optional(),
|
|
76
82
|
executable: z.string().min(1),
|
|
77
83
|
args: z.array(z.string()).default([]),
|
|
78
84
|
stdin: launchStdinSchema.optional(),
|
|
@@ -573,6 +579,13 @@ export const backgroundExecInput = z.union([
|
|
|
573
579
|
const backgroundResult = readOutput.extend({
|
|
574
580
|
timedOut: z.boolean(),
|
|
575
581
|
signalled: z.boolean().optional(),
|
|
582
|
+
sandbox: z
|
|
583
|
+
.strictObject({
|
|
584
|
+
mode: z.enum(["full-access", "workspace", "read-only"]),
|
|
585
|
+
likelyDenied: z.boolean(),
|
|
586
|
+
ranOutsideSandbox: z.boolean(),
|
|
587
|
+
})
|
|
588
|
+
.optional(),
|
|
576
589
|
});
|
|
577
590
|
export const backgroundExecOutput = z.union([backgroundResult, listOutput]);
|
|
578
591
|
/** Reuse Session ownership and logs; a wait deadline never cancels the command. */
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
+
import { type NativePayloadStaging } from "./private-local-native-staging.js";
|
|
1
2
|
import { z } from "zod";
|
|
3
|
+
import type { LocalPermissionMode } from "./private-local-permission-mode.js";
|
|
2
4
|
export declare const sandboxUnavailableReasonSchema: z.ZodObject<{
|
|
3
5
|
kind: z.ZodEnum<{
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
"
|
|
8
|
-
"unsupported-platform": "unsupported-platform";
|
|
9
|
-
"invalid-folder": "invalid-folder";
|
|
10
|
-
"backend-refused": "backend-refused";
|
|
6
|
+
install: "install";
|
|
7
|
+
unsupported: "unsupported";
|
|
8
|
+
folder: "folder";
|
|
9
|
+
"startup-refused": "startup-refused";
|
|
11
10
|
}>;
|
|
12
11
|
message: z.ZodString;
|
|
13
12
|
}, z.core.$strict>;
|
|
@@ -22,13 +21,14 @@ export type SandboxCommand = Readonly<{
|
|
|
22
21
|
args: readonly string[];
|
|
23
22
|
environment: Readonly<{
|
|
24
23
|
TMPDIR: string;
|
|
24
|
+
SSH_AUTH_SOCK?: string;
|
|
25
25
|
}>;
|
|
26
26
|
/** Linux only: feed these verified bytes to fd3; never inherit arbitrary fds. */
|
|
27
27
|
seccompFilter?: Uint8Array;
|
|
28
28
|
}>;
|
|
29
29
|
/** Existing foreground/background owners retain process and cancellation lifetime. */
|
|
30
30
|
export type LocalSandbox = Readonly<{
|
|
31
|
-
command(cwd: string, argv: readonly [string, ...string[]]): SandboxCommand;
|
|
31
|
+
command(cwd: string, argv: readonly [string, ...string[]], readOnlyHelperFile?: string): SandboxCommand;
|
|
32
32
|
close(): void;
|
|
33
33
|
}>;
|
|
34
34
|
export type SandboxAdmission = Readonly<{
|
|
@@ -38,6 +38,7 @@ export type SandboxAdmission = Readonly<{
|
|
|
38
38
|
state: "unavailable";
|
|
39
39
|
reason: SandboxUnavailableReason;
|
|
40
40
|
}>;
|
|
41
|
+
export declare function sandboxUnavailableReason(cause: unknown): SandboxUnavailableReason;
|
|
41
42
|
/**
|
|
42
43
|
* The caller already owns the person's auto-approve choice. This module has no
|
|
43
44
|
* mode, setting, bare-command branch, PATH fallback or permission to widen it.
|
|
@@ -47,5 +48,14 @@ export declare function establishLocalSandbox(input: Readonly<{
|
|
|
47
48
|
project: string;
|
|
48
49
|
/** Absolute folder selected by the Local owner. */
|
|
49
50
|
folder: string;
|
|
51
|
+
mode: Exclude<LocalPermissionMode, "full-access">;
|
|
52
|
+
temporaryDirectory: string;
|
|
50
53
|
platform?: string;
|
|
54
|
+
}>, staging?: NativePayloadStaging,
|
|
55
|
+
/** Host startup probe; a Linux command-generation test can supply its own runner. */
|
|
56
|
+
runProbe?: (command: SandboxCommand) => Readonly<{
|
|
57
|
+
status: number | null;
|
|
58
|
+
stderr: string;
|
|
59
|
+
signal: string | null;
|
|
60
|
+
error?: Error;
|
|
51
61
|
}>): SandboxAdmission;
|
|
@@ -1,22 +1,14 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { closeSync,
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import { closeSync, openSync, realpathSync } from "node:fs";
|
|
3
|
+
import { userInfo } from "node:os";
|
|
4
|
+
import { LocalBwrapUnavailable } from "./private-local-bwrap.js";
|
|
5
|
+
import { createNativePayloadStaging, } from "./private-local-native-staging.js";
|
|
6
6
|
import { linuxSandboxCommand } from "./private-local-linux-sandbox.js";
|
|
7
|
-
import { seatbeltCommand } from "./private-local-seatbelt.js";
|
|
8
|
-
import { sandboxPolicy } from "./private-local-sandbox-policy.js";
|
|
7
|
+
import { seatbeltCommand, UnsafeSeatbeltFolder, } from "./private-local-seatbelt.js";
|
|
8
|
+
import { sandboxPolicy, linuxSandboxProfile, UnsafeGitMarker, UnsafeHostCode, UnsafeNativePayload, } from "./private-local-sandbox-policy.js";
|
|
9
9
|
import { z } from "zod";
|
|
10
10
|
export const sandboxUnavailableReasonSchema = z.strictObject({
|
|
11
|
-
kind: z.enum([
|
|
12
|
-
"unsupported-platform",
|
|
13
|
-
"invalid-folder",
|
|
14
|
-
"missing",
|
|
15
|
-
"invalid-digest",
|
|
16
|
-
"digest-mismatch",
|
|
17
|
-
"not-executable",
|
|
18
|
-
"backend-refused",
|
|
19
|
-
]),
|
|
11
|
+
kind: z.enum(["unsupported", "install", "folder", "startup-refused"]),
|
|
20
12
|
message: z.string(),
|
|
21
13
|
});
|
|
22
14
|
export class LocalSandboxUnavailable extends Error {
|
|
@@ -27,11 +19,19 @@ export class LocalSandboxUnavailable extends Error {
|
|
|
27
19
|
this.reason = reason;
|
|
28
20
|
}
|
|
29
21
|
}
|
|
30
|
-
function
|
|
22
|
+
export function sandboxUnavailableReason(cause) {
|
|
23
|
+
if (cause instanceof UnsafeGitMarker)
|
|
24
|
+
return { kind: "folder", message: cause.message };
|
|
25
|
+
if (cause instanceof UnsafeSeatbeltFolder)
|
|
26
|
+
return { kind: "folder", message: cause.message };
|
|
27
|
+
if (cause instanceof UnsafeHostCode)
|
|
28
|
+
return { kind: "install", message: cause.message };
|
|
29
|
+
if (cause instanceof UnsafeNativePayload)
|
|
30
|
+
return { kind: "install", message: cause.message };
|
|
31
31
|
if (cause instanceof LocalBwrapUnavailable)
|
|
32
32
|
return { kind: cause.reason, message: cause.message };
|
|
33
33
|
return {
|
|
34
|
-
kind: "
|
|
34
|
+
kind: "startup-refused",
|
|
35
35
|
message: cause instanceof Error ? cause.message : "Sandbox preparation failed.",
|
|
36
36
|
};
|
|
37
37
|
}
|
|
@@ -40,7 +40,9 @@ function unavailable(cause) {
|
|
|
40
40
|
* mode, setting, bare-command branch, PATH fallback or permission to widen it.
|
|
41
41
|
* A ready result proves only that the backend can start, not the escape matrix.
|
|
42
42
|
*/
|
|
43
|
-
export function establishLocalSandbox(input
|
|
43
|
+
export function establishLocalSandbox(input, staging,
|
|
44
|
+
/** Host startup probe; a Linux command-generation test can supply its own runner. */
|
|
45
|
+
runProbe) {
|
|
44
46
|
try {
|
|
45
47
|
sandboxPolicy(input.project, input.folder);
|
|
46
48
|
}
|
|
@@ -48,7 +50,7 @@ export function establishLocalSandbox(input) {
|
|
|
48
50
|
return {
|
|
49
51
|
state: "unavailable",
|
|
50
52
|
reason: {
|
|
51
|
-
kind: "
|
|
53
|
+
kind: "folder",
|
|
52
54
|
message: cause instanceof Error ? cause.message : "Invalid sandbox folder.",
|
|
53
55
|
},
|
|
54
56
|
};
|
|
@@ -58,44 +60,66 @@ export function establishLocalSandbox(input) {
|
|
|
58
60
|
return {
|
|
59
61
|
state: "unavailable",
|
|
60
62
|
reason: {
|
|
61
|
-
kind: "unsupported
|
|
62
|
-
message:
|
|
63
|
+
kind: "unsupported",
|
|
64
|
+
message: `${input.mode === "workspace" ? "Workspace" : "Read-only"} on ${platform} needs a supported OS sandbox; use --yolo or run on macOS/Linux.`,
|
|
63
65
|
},
|
|
64
66
|
};
|
|
65
|
-
let
|
|
67
|
+
let native;
|
|
66
68
|
try {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
native =
|
|
70
|
+
staging ??
|
|
71
|
+
(platform === "linux"
|
|
72
|
+
? createNativePayloadStaging(input.project)
|
|
73
|
+
: undefined);
|
|
74
|
+
const environmentTmp = platform === "linux" ? "/tmp" : input.temporaryDirectory;
|
|
70
75
|
let closed = false;
|
|
71
76
|
const sandbox = {
|
|
72
|
-
command: (cwd, argv) => {
|
|
77
|
+
command: (cwd, argv, readOnlyHelperFile) => {
|
|
73
78
|
if (closed)
|
|
74
79
|
throw new LocalSandboxUnavailable({
|
|
75
|
-
kind: "
|
|
80
|
+
kind: "startup-refused",
|
|
76
81
|
message: "Sandbox has been closed.",
|
|
77
82
|
});
|
|
78
83
|
// Failed/incomplete authority discovery is not an unavailable backend:
|
|
79
84
|
// it must not turn into an authorized unsandboxed retry.
|
|
80
85
|
const policy = sandboxPolicy(input.project, input.folder);
|
|
81
86
|
try {
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
87
|
+
const profile = linuxSandboxProfile(policy, input.mode, input.temporaryDirectory, process.env, userInfo().homedir, platform);
|
|
88
|
+
const linuxPayload = platform === "linux" ? native?.linux(profile) : undefined;
|
|
89
|
+
const stagedHelper = readOnlyHelperFile === undefined
|
|
90
|
+
? undefined
|
|
91
|
+
: native?.patch(readOnlyHelperFile);
|
|
92
|
+
const stagedArgv = stagedHelper === undefined
|
|
93
|
+
? argv
|
|
94
|
+
: [
|
|
95
|
+
argv[0] === readOnlyHelperFile ? stagedHelper : argv[0],
|
|
96
|
+
...argv
|
|
97
|
+
.slice(1)
|
|
98
|
+
.map((argument) => argument === readOnlyHelperFile ? stagedHelper : argument),
|
|
99
|
+
];
|
|
100
|
+
const launch = linuxPayload !== undefined
|
|
101
|
+
? linuxSandboxCommand(profile, linuxPayload, cwd, stagedArgv, undefined, stagedHelper)
|
|
102
|
+
: seatbeltCommand(profile, stagedArgv);
|
|
103
|
+
let environment = {
|
|
104
|
+
TMPDIR: environmentTmp,
|
|
88
105
|
};
|
|
106
|
+
if (platform === "linux" && profile.sshAgentSocket !== undefined)
|
|
107
|
+
environment = {
|
|
108
|
+
TMPDIR: environmentTmp,
|
|
109
|
+
SSH_AUTH_SOCK: profile.sshAgentSocket,
|
|
110
|
+
};
|
|
111
|
+
return { ...launch, environment };
|
|
89
112
|
}
|
|
90
113
|
catch (cause) {
|
|
91
|
-
throw new LocalSandboxUnavailable(
|
|
114
|
+
throw new LocalSandboxUnavailable(sandboxUnavailableReason(cause));
|
|
92
115
|
}
|
|
93
116
|
},
|
|
94
117
|
close: () => {
|
|
95
118
|
if (closed)
|
|
96
119
|
return;
|
|
97
120
|
closed = true;
|
|
98
|
-
|
|
121
|
+
if (staging === undefined)
|
|
122
|
+
native?.close();
|
|
99
123
|
},
|
|
100
124
|
};
|
|
101
125
|
const command = sandbox.command(realpathSync(input.folder), [
|
|
@@ -104,8 +128,9 @@ export function establishLocalSandbox(input) {
|
|
|
104
128
|
let filterDescriptor;
|
|
105
129
|
const probe = () => {
|
|
106
130
|
if (command.seccompFilter !== undefined) {
|
|
107
|
-
const path =
|
|
108
|
-
|
|
131
|
+
const path = native?.probeFilter(command.seccompFilter);
|
|
132
|
+
if (path === undefined)
|
|
133
|
+
throw new Error("No Linux filter staging owner");
|
|
109
134
|
filterDescriptor = openSync(path, "r");
|
|
110
135
|
}
|
|
111
136
|
return spawnSync(command.executable, [...command.args], {
|
|
@@ -125,7 +150,7 @@ export function establishLocalSandbox(input) {
|
|
|
125
150
|
};
|
|
126
151
|
const result = (() => {
|
|
127
152
|
try {
|
|
128
|
-
return probe();
|
|
153
|
+
return runProbe?.(command) ?? probe();
|
|
129
154
|
}
|
|
130
155
|
finally {
|
|
131
156
|
if (filterDescriptor !== undefined)
|
|
@@ -134,28 +159,28 @@ export function establishLocalSandbox(input) {
|
|
|
134
159
|
})();
|
|
135
160
|
if (result.status !== 0) {
|
|
136
161
|
sandbox.close();
|
|
137
|
-
const stderr = result.stderr.trim();
|
|
162
|
+
const stderr = result.error === undefined ? result.stderr.trim() : "";
|
|
138
163
|
return {
|
|
139
164
|
state: "unavailable",
|
|
140
165
|
reason: {
|
|
141
|
-
kind: "
|
|
142
|
-
message: result.error?.message ??
|
|
166
|
+
kind: "startup-refused",
|
|
167
|
+
message: `${input.mode === "workspace" ? "Workspace" : "Read-only"} on this ${platform === "darwin" ? "Mac" : "Linux host"} needs a working ${platform === "darwin" ? "sandbox-exec profile probe" : "bwrap installation"}; use --yolo or ${platform === "darwin" ? "check the failed Seatbelt profile probe" : "fix the sandbox installation"}. ${result.error?.message ??
|
|
143
168
|
(stderr.length > 0
|
|
144
169
|
? stderr
|
|
145
|
-
: `Sandbox startup exited ${String(result.status)} (${String(result.signal)}).`)
|
|
170
|
+
: `Sandbox startup exited ${String(result.status)} (${String(result.signal)}).`)}`,
|
|
146
171
|
},
|
|
147
172
|
};
|
|
148
173
|
}
|
|
149
174
|
return { state: "ready", sandbox };
|
|
150
175
|
}
|
|
151
176
|
catch (cause) {
|
|
152
|
-
if (
|
|
153
|
-
|
|
177
|
+
if (staging === undefined)
|
|
178
|
+
native?.close();
|
|
154
179
|
return {
|
|
155
180
|
state: "unavailable",
|
|
156
181
|
reason: cause instanceof LocalSandboxUnavailable
|
|
157
182
|
? cause.reason
|
|
158
|
-
:
|
|
183
|
+
: sandboxUnavailableReason(cause),
|
|
159
184
|
};
|
|
160
185
|
}
|
|
161
186
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BirdDogMail } from "@fungi.computer/bird-dog";
|
|
2
|
-
import type { Accepted, CancelResult, CommandSettlement, VerifiedAgentReply } from "@fungi.computer/shiitake";
|
|
2
|
+
import type { Accepted, CancelResult, CommandSettlement, DelegationReceipt, VerifiedAgentReply } from "@fungi.computer/shiitake";
|
|
3
3
|
import type { AgentControlDirectory, AgentDelegationInput, AgentDelegationInvocation, AgentRecallInput } from "@fungi.computer/shiitake/agent-control";
|
|
4
4
|
import { createFleetDelegation } from "@fungi.computer/shiitake/fleet";
|
|
5
5
|
/**
|
|
@@ -8,7 +8,7 @@ import { createFleetDelegation } from "@fungi.computer/shiitake/fleet";
|
|
|
8
8
|
* owner when it is idle; `readSettlement` reads one exact command there.
|
|
9
9
|
*/
|
|
10
10
|
export type PrivateLocalMailRouting = Readonly<{
|
|
11
|
-
deliver: (mail: BirdDogMail) => Promise<
|
|
11
|
+
deliver: (mail: BirdDogMail) => Promise<DelegationReceipt>;
|
|
12
12
|
readSettlement: (agentId: string, sessionId: string, commandId: string) => Promise<CommandSettlement | undefined>;
|
|
13
13
|
}>;
|
|
14
14
|
/**
|
|
@@ -29,7 +29,7 @@ export declare function createPrivateLocalDelegation(input: Readonly<{
|
|
|
29
29
|
}[] | undefined>) | undefined;
|
|
30
30
|
accept: (mail: BirdDogMail, meta?: BirdDogMail["meta"], reply?: VerifiedAgentReply) => Promise<Accepted>;
|
|
31
31
|
}>): {
|
|
32
|
-
send: (task: AgentDelegationInput, sourceSessionId: string, signal: AbortSignal, invocation?: AgentDelegationInvocation) => Promise<
|
|
32
|
+
send: (task: AgentDelegationInput, sourceSessionId: string, signal: AbortSignal, invocation?: AgentDelegationInvocation) => Promise<DelegationReceipt>;
|
|
33
33
|
recall: (task: AgentRecallInput, sourceSessionId: string, signal: AbortSignal) => Promise<CancelResult>;
|
|
34
34
|
receive: (mail: BirdDogMail) => Promise<Accepted>;
|
|
35
35
|
outstanding: () => Promise<import("@fungi.computer/shiitake/fleet").OutstandingDelegation | undefined>;
|