@fungi.computer/truffle 0.2.2 → 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 +3 -17
- package/dist/private-invocation-computer.d.ts +224 -12
- 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 +9 -7
- package/dist/private-local-bwrap.js +26 -21
- 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 +15 -13
- package/dist/private-local-containment.js +49 -65
- 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 -5
- package/dist/private-local-linux-sandbox.js +63 -30
- 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 +1 -1
- 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 +7 -8
- package/dist/private-local-native-staging.js +132 -43
- 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 +378 -65
- package/dist/private-local-sandbox-policy.d.ts +30 -11
- package/dist/private-local-sandbox-policy.js +223 -373
- package/dist/private-local-sandbox.d.ts +30 -25
- package/dist/private-local-sandbox.js +190 -100
- 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 +19 -7
- 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 +30 -3
- package/dist/private-local-session-discovery.d.ts +2 -1
- 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 +2 -0
- 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 +23 -17
|
@@ -1,48 +1,53 @@
|
|
|
1
1
|
import type { NativeSourceReader } from "./private-local-bwrap.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { sandboxUnavailableReasonSchema, type SandboxUnavailableReason } from "./private-local-containment.js";
|
|
4
|
+
import type { LocalPermissionMode } from "./private-local-permission-mode.js";
|
|
3
5
|
import type { LocalSandboxWrites } from "./private-invocation-files.js";
|
|
4
|
-
import {
|
|
6
|
+
import type { NativePayloadStaging } from "./private-local-native-staging.js";
|
|
5
7
|
/** Availability is not a hostile-program security or an approval status. */
|
|
6
|
-
export declare const sandboxStatusSchema: z.
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
export declare const sandboxStatusSchema: z.ZodObject<{
|
|
9
|
+
mode: z.ZodEnum<{
|
|
10
|
+
"read-only": "read-only";
|
|
11
|
+
workspace: "workspace";
|
|
12
|
+
"full-access": "full-access";
|
|
13
|
+
}>;
|
|
14
|
+
available: z.ZodBoolean;
|
|
15
|
+
reason: z.ZodOptional<z.ZodObject<{
|
|
13
16
|
kind: z.ZodEnum<{
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"digest-mismatch": "digest-mismatch";
|
|
18
|
-
unreadable: "unreadable";
|
|
19
|
-
"not-executable": "not-executable";
|
|
20
|
-
"staging-failed": "staging-failed";
|
|
21
|
-
"unsupported-platform": "unsupported-platform";
|
|
22
|
-
"invalid-folder": "invalid-folder";
|
|
23
|
-
"unsafe-native-payload": "unsafe-native-payload";
|
|
24
|
-
"unsafe-host-code": "unsafe-host-code";
|
|
17
|
+
install: "install";
|
|
18
|
+
unsupported: "unsupported";
|
|
19
|
+
folder: "folder";
|
|
25
20
|
"startup-refused": "startup-refused";
|
|
26
|
-
"backend-refused": "backend-refused";
|
|
27
21
|
}>;
|
|
28
22
|
message: z.ZodString;
|
|
29
|
-
}, z.core.$strict
|
|
30
|
-
}, z.core.$strict
|
|
23
|
+
}, z.core.$strict>>;
|
|
24
|
+
}, z.core.$strict>;
|
|
31
25
|
export type LocalSandboxStatus = z.output<typeof sandboxStatusSchema>;
|
|
32
26
|
export type LocalSandboxControls = LocalSandboxWrites & Readonly<{
|
|
33
27
|
assertReady(): void;
|
|
28
|
+
assertModeReady(mode: LocalPermissionMode): void;
|
|
34
29
|
status(): LocalSandboxStatus;
|
|
35
30
|
close(): void;
|
|
36
31
|
}>;
|
|
32
|
+
/** Refusal before an Agent Files write can be staged or dispatched. */
|
|
33
|
+
export declare class ProtectedLocalFilesPath extends Error {
|
|
34
|
+
readonly path: string;
|
|
35
|
+
readonly name = "ProtectedLocalFilesPath";
|
|
36
|
+
constructor(path: string);
|
|
37
|
+
}
|
|
37
38
|
/**
|
|
38
|
-
* Native Local owns the live
|
|
39
|
+
* Native Local owns the live mode; the adapter owns only preparation.
|
|
39
40
|
* Every consumer calls this authority immediately before launching or writing.
|
|
40
41
|
*/
|
|
41
42
|
export declare function createLocalSandboxControls(input: Readonly<{
|
|
42
43
|
project: string;
|
|
43
44
|
folder: string;
|
|
44
|
-
|
|
45
|
+
agentId?: string;
|
|
46
|
+
mode(): LocalPermissionMode;
|
|
47
|
+
defaultReason?: z.output<typeof sandboxUnavailableReasonSchema> | undefined;
|
|
45
48
|
platform?: string;
|
|
46
49
|
readNativePatchSource?: NativeSourceReader;
|
|
47
|
-
|
|
50
|
+
native?: NativePayloadStaging | undefined;
|
|
48
51
|
}>): LocalSandboxControls;
|
|
52
|
+
/** Probe a fresh Agent before deciding its mode; never treat an unavailable backend as a sandboxed run. */
|
|
53
|
+
export declare function probeLocalSandbox(project: string, folder: string, native?: NativePayloadStaging): SandboxUnavailableReason | undefined;
|
|
@@ -1,70 +1,130 @@
|
|
|
1
|
-
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
1
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { rmSync } from "node:fs";
|
|
4
4
|
import { createNativePayloadStaging } from "./private-local-native-staging.js";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { establishLocalSandbox, LocalSandboxUnavailable, sandboxUnavailableReason, sandboxUnavailableReasonSchema, } from "./private-local-containment.js";
|
|
7
|
-
import { protectedSandboxPaths, assertHostCodeOutsideAgent, UnsafeHostCode, resolveAgentFolder, sandboxPolicy, } from "./private-local-sandbox-policy.js";
|
|
7
|
+
import { protectedSandboxPaths, assertHostCodeOutsideAgent, UnsafeHostCode, resolveAgentFolder, sandboxPolicy, agentSandboxTemporaryDirectory, } from "./private-local-sandbox-policy.js";
|
|
8
8
|
/** Availability is not a hostile-program security or an approval status. */
|
|
9
|
-
export const sandboxStatusSchema = z.
|
|
10
|
-
z.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
9
|
+
export const sandboxStatusSchema = z.strictObject({
|
|
10
|
+
mode: z.enum(["full-access", "workspace", "read-only"]),
|
|
11
|
+
available: z.boolean(),
|
|
12
|
+
reason: sandboxUnavailableReasonSchema.optional(),
|
|
13
|
+
});
|
|
14
|
+
/** Refusal before an Agent Files write can be staged or dispatched. */
|
|
15
|
+
export class ProtectedLocalFilesPath extends Error {
|
|
16
|
+
path;
|
|
17
|
+
name = "ProtectedLocalFilesPath";
|
|
18
|
+
constructor(path) {
|
|
19
|
+
super(`Files write targets a protected Local path: ${path}`);
|
|
20
|
+
this.path = path;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
20
23
|
function contains(root, path) {
|
|
21
24
|
const within = relative(root, path);
|
|
22
25
|
return (!isAbsolute(within) && within !== ".." && !within.startsWith(`..${sep}`));
|
|
23
26
|
}
|
|
24
27
|
/**
|
|
25
|
-
* Native Local owns the live
|
|
28
|
+
* Native Local owns the live mode; the adapter owns only preparation.
|
|
26
29
|
* Every consumer calls this authority immediately before launching or writing.
|
|
27
30
|
*/
|
|
28
31
|
export function createLocalSandboxControls(input) {
|
|
29
|
-
const
|
|
32
|
+
const mode = input.mode();
|
|
30
33
|
const folder = resolveAgentFolder(input.project, input.folder);
|
|
34
|
+
// Before an Agent store exists, a probe has a disposable temporary owner,
|
|
35
|
+
// not a fabricated Local store identity.
|
|
36
|
+
const temporaryOwner = input.agentId ??
|
|
37
|
+
`probe-${String(process.pid)}-${process.hrtime.bigint().toString(36)}`;
|
|
31
38
|
// The packed Node wrapper passes the paths it actually loaded. In source
|
|
32
39
|
// runs this module is the executing host authority instead. Check before
|
|
33
|
-
// staging or probing
|
|
40
|
+
// staging or probing. Full access never calls this admission.
|
|
41
|
+
const assertHostCode = () => {
|
|
42
|
+
try {
|
|
43
|
+
const launcher = process.env.FUNGI_LAUNCHER;
|
|
44
|
+
const cli = launcher === undefined
|
|
45
|
+
? fileURLToPath(import.meta.url)
|
|
46
|
+
: process.env.FUNGI_BUNDLED_CLI;
|
|
47
|
+
if (cli === undefined)
|
|
48
|
+
throw new UnsafeHostCode("Packed CLI entry was not provided by the launcher; reinstall Fungi outside the project.");
|
|
49
|
+
assertHostCodeOutsideAgent(input.project, cli, launcher ?? cli);
|
|
50
|
+
}
|
|
51
|
+
catch (cause) {
|
|
52
|
+
throw new LocalSandboxUnavailable(cause instanceof UnsafeHostCode
|
|
53
|
+
? sandboxUnavailableReason(cause)
|
|
54
|
+
: {
|
|
55
|
+
kind: "install",
|
|
56
|
+
message: `Fungi host JavaScript identity cannot be verified (${cause instanceof Error ? cause.message : String(cause)}); reinstall Fungi using a copying install outside the project.`,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
if (mode !== "full-access")
|
|
61
|
+
assertHostCode();
|
|
62
|
+
let hostCodeAdmitted = mode !== "full-access";
|
|
63
|
+
let temporaryDirectory = mode === "full-access"
|
|
64
|
+
? undefined
|
|
65
|
+
: agentSandboxTemporaryDirectory(temporaryOwner, input.project);
|
|
66
|
+
const platform = input.platform === undefined ? {} : { platform: input.platform };
|
|
67
|
+
const supportedPlatform = (input.platform ?? process.platform) === "linux" ||
|
|
68
|
+
(input.platform ?? process.platform) === "darwin";
|
|
69
|
+
const ownNative = () => {
|
|
70
|
+
try {
|
|
71
|
+
return createNativePayloadStaging(input.project, input.readNativePatchSource);
|
|
72
|
+
}
|
|
73
|
+
catch (cause) {
|
|
74
|
+
throw new LocalSandboxUnavailable(sandboxUnavailableReason(cause));
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
const admitNative = (staging) => {
|
|
78
|
+
try {
|
|
79
|
+
staging?.admitProject(input.project);
|
|
80
|
+
}
|
|
81
|
+
catch (cause) {
|
|
82
|
+
throw new LocalSandboxUnavailable(sandboxUnavailableReason(cause));
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
let native;
|
|
86
|
+
let nativeAdmitted = false;
|
|
87
|
+
let initial;
|
|
34
88
|
try {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
89
|
+
if (mode !== "full-access") {
|
|
90
|
+
native = supportedPlatform ? (input.native ?? ownNative()) : undefined;
|
|
91
|
+
admitNative(native);
|
|
92
|
+
nativeAdmitted = native !== undefined;
|
|
93
|
+
initial = establishLocalSandbox({
|
|
94
|
+
project: input.project,
|
|
95
|
+
folder: folder.absolute,
|
|
96
|
+
mode,
|
|
97
|
+
temporaryDirectory: temporaryDirectory ??
|
|
98
|
+
agentSandboxTemporaryDirectory(temporaryOwner, input.project),
|
|
99
|
+
...platform,
|
|
100
|
+
}, native);
|
|
101
|
+
}
|
|
42
102
|
}
|
|
43
103
|
catch (cause) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
104
|
+
try {
|
|
105
|
+
if (input.native === undefined)
|
|
106
|
+
native?.close();
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
if (temporaryDirectory !== undefined)
|
|
110
|
+
rmSync(temporaryDirectory, { recursive: true, force: true });
|
|
111
|
+
}
|
|
112
|
+
throw cause;
|
|
50
113
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
project: input.project,
|
|
55
|
-
folder: folder.absolute,
|
|
56
|
-
...platform,
|
|
57
|
-
}, native);
|
|
58
|
-
let sandbox = initial.state === "ready" ? initial.sandbox : undefined;
|
|
59
|
-
let everReady = initial.state === "ready";
|
|
60
|
-
let unavailable = initial.state === "unavailable" ? initial.reason : undefined;
|
|
114
|
+
let sandbox = initial?.state === "ready" ? initial.sandbox : undefined;
|
|
115
|
+
let preparedMode = sandbox === undefined ? undefined : mode;
|
|
116
|
+
let unavailable = initial?.state === "unavailable" ? initial.reason : input.defaultReason;
|
|
61
117
|
if (unavailable !== undefined) {
|
|
62
|
-
if (
|
|
63
|
-
|
|
118
|
+
if (temporaryDirectory !== undefined)
|
|
119
|
+
rmSync(temporaryDirectory, { recursive: true, force: true });
|
|
120
|
+
if (unavailable.kind === "folder") {
|
|
121
|
+
if (input.native === undefined)
|
|
122
|
+
native?.close();
|
|
64
123
|
throw new Error(unavailable.message);
|
|
65
124
|
}
|
|
66
|
-
if (
|
|
67
|
-
native
|
|
125
|
+
if (mode !== "full-access") {
|
|
126
|
+
if (input.native === undefined)
|
|
127
|
+
native?.close();
|
|
68
128
|
throw new LocalSandboxUnavailable(unavailable);
|
|
69
129
|
}
|
|
70
130
|
}
|
|
@@ -79,30 +139,50 @@ export function createLocalSandboxControls(input) {
|
|
|
79
139
|
throw new Error("Local Agent folder authority changed while open");
|
|
80
140
|
}
|
|
81
141
|
catch (cause) {
|
|
82
|
-
if (!everReady)
|
|
83
|
-
throw cause;
|
|
84
142
|
unavailable = {
|
|
85
|
-
kind: "
|
|
143
|
+
kind: "folder",
|
|
86
144
|
message: cause instanceof Error ? cause.message : String(cause),
|
|
87
145
|
};
|
|
88
146
|
throw new LocalSandboxUnavailable(unavailable);
|
|
89
147
|
}
|
|
90
148
|
};
|
|
149
|
+
function prepareSandbox(nextMode) {
|
|
150
|
+
if (sandbox !== undefined && preparedMode === nextMode)
|
|
151
|
+
return;
|
|
152
|
+
sandbox?.close();
|
|
153
|
+
sandbox = undefined;
|
|
154
|
+
if (!hostCodeAdmitted) {
|
|
155
|
+
assertHostCode();
|
|
156
|
+
hostCodeAdmitted = true;
|
|
157
|
+
}
|
|
158
|
+
native ??= input.native ?? ownNative();
|
|
159
|
+
if (!nativeAdmitted) {
|
|
160
|
+
admitNative(native);
|
|
161
|
+
nativeAdmitted = true;
|
|
162
|
+
}
|
|
163
|
+
temporaryDirectory ??= agentSandboxTemporaryDirectory(temporaryOwner, input.project);
|
|
164
|
+
const admission = establishLocalSandbox({
|
|
165
|
+
project: input.project,
|
|
166
|
+
folder: folder.absolute,
|
|
167
|
+
mode: nextMode,
|
|
168
|
+
temporaryDirectory,
|
|
169
|
+
...platform,
|
|
170
|
+
}, native);
|
|
171
|
+
if (admission.state === "ready") {
|
|
172
|
+
sandbox = admission.sandbox;
|
|
173
|
+
preparedMode = nextMode;
|
|
174
|
+
unavailable = undefined;
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
unavailable = admission.reason;
|
|
178
|
+
throw new LocalSandboxUnavailable(unavailable);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
91
181
|
function launch(cwd, argv, readOnlyHelperFile) {
|
|
92
182
|
assertFolder();
|
|
93
|
-
if (
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
folder: folder.absolute,
|
|
97
|
-
...platform,
|
|
98
|
-
}, native);
|
|
99
|
-
if (admission.state === "ready") {
|
|
100
|
-
sandbox = admission.sandbox;
|
|
101
|
-
everReady = true;
|
|
102
|
-
}
|
|
103
|
-
else
|
|
104
|
-
unavailable = admission.reason;
|
|
105
|
-
}
|
|
183
|
+
if (input.mode() === "full-access")
|
|
184
|
+
return undefined;
|
|
185
|
+
prepareSandbox(input.mode() === "read-only" ? "read-only" : "workspace");
|
|
106
186
|
if (sandbox !== undefined) {
|
|
107
187
|
try {
|
|
108
188
|
const command = sandbox.command(cwd, argv, readOnlyHelperFile);
|
|
@@ -119,60 +199,49 @@ export function createLocalSandboxControls(input) {
|
|
|
119
199
|
}
|
|
120
200
|
if (unavailable === undefined)
|
|
121
201
|
throw new Error("Local sandbox preparation produced no result");
|
|
122
|
-
if (unavailable.kind === "
|
|
202
|
+
if (unavailable.kind === "folder")
|
|
123
203
|
throw new Error(unavailable.message);
|
|
124
|
-
|
|
125
|
-
// Before readiness, only availability failures permit fallback.
|
|
126
|
-
if (everReady ||
|
|
127
|
-
(unavailable.kind !== "missing" &&
|
|
128
|
-
unavailable.kind !== "unsupported-platform" &&
|
|
129
|
-
unavailable.kind !== "startup-refused"))
|
|
130
|
-
throw new LocalSandboxUnavailable(unavailable);
|
|
131
|
-
if (input.required())
|
|
132
|
-
throw new LocalSandboxUnavailable(unavailable);
|
|
133
|
-
// Files is never handed back its installed executable for a bare retry.
|
|
134
|
-
if (readOnlyHelperFile !== undefined) {
|
|
135
|
-
try {
|
|
136
|
-
return {
|
|
137
|
-
bareNative: true,
|
|
138
|
-
executable: native.patch(readOnlyHelperFile),
|
|
139
|
-
args: [],
|
|
140
|
-
environment: { TMPDIR: process.env.TMPDIR ?? tmpdir() },
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
catch (cause) {
|
|
144
|
-
unavailable = sandboxUnavailableReason(cause);
|
|
145
|
-
throw new LocalSandboxUnavailable(unavailable);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
// No executable has started. Generic exec retains its ordinary fallback.
|
|
149
|
-
return undefined;
|
|
204
|
+
throw new LocalSandboxUnavailable(unavailable);
|
|
150
205
|
}
|
|
151
206
|
const assertReady = () => {
|
|
207
|
+
if (input.mode() === "full-access")
|
|
208
|
+
return;
|
|
152
209
|
launch(folder.absolute, ["/usr/bin/true"]);
|
|
153
210
|
};
|
|
154
211
|
return {
|
|
155
212
|
launch,
|
|
156
213
|
assertReady,
|
|
157
|
-
|
|
214
|
+
assertModeReady: (nextMode) => {
|
|
215
|
+
if (nextMode === "full-access")
|
|
216
|
+
return;
|
|
217
|
+
assertFolder();
|
|
218
|
+
prepareSandbox(nextMode);
|
|
219
|
+
if (sandbox === undefined)
|
|
220
|
+
throw new Error("Local sandbox preparation produced no result");
|
|
221
|
+
sandbox.command(folder.absolute, ["/usr/bin/true"]);
|
|
222
|
+
},
|
|
223
|
+
assertWrite: (paths) => {
|
|
224
|
+
if (input.mode() === "full-access")
|
|
225
|
+
return;
|
|
158
226
|
assertReady();
|
|
159
|
-
const target = resolve(path);
|
|
160
227
|
const policy = sandboxPolicy(input.project, folder.absolute);
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
228
|
+
const protectedPaths = protectedSandboxPaths([
|
|
229
|
+
...policy.readOnly,
|
|
230
|
+
join(policy.writableRoot, ".git"),
|
|
231
|
+
]);
|
|
232
|
+
for (const path of paths) {
|
|
233
|
+
const target = resolve(path);
|
|
234
|
+
if (!contains(policy.writableRoot, target) ||
|
|
235
|
+
protectedPaths.some((protectedPath) => contains(protectedPath, target)))
|
|
236
|
+
throw new ProtectedLocalFilesPath(target);
|
|
237
|
+
}
|
|
165
238
|
},
|
|
166
239
|
status: () => {
|
|
167
240
|
if (closed)
|
|
168
241
|
throw new Error("Local sandbox owner is closed");
|
|
169
242
|
return unavailable === undefined
|
|
170
|
-
? {
|
|
171
|
-
: {
|
|
172
|
-
state: "unavailable",
|
|
173
|
-
requireSandbox: input.required(),
|
|
174
|
-
reason: unavailable,
|
|
175
|
-
};
|
|
243
|
+
? { mode: input.mode(), available: true }
|
|
244
|
+
: { mode: input.mode(), available: false, reason: unavailable };
|
|
176
245
|
},
|
|
177
246
|
close: () => {
|
|
178
247
|
if (closed)
|
|
@@ -182,8 +251,29 @@ export function createLocalSandboxControls(input) {
|
|
|
182
251
|
sandbox?.close();
|
|
183
252
|
}
|
|
184
253
|
finally {
|
|
185
|
-
native
|
|
254
|
+
if (input.native === undefined)
|
|
255
|
+
native?.close();
|
|
256
|
+
if (temporaryDirectory !== undefined)
|
|
257
|
+
rmSync(temporaryDirectory, { recursive: true });
|
|
186
258
|
}
|
|
187
259
|
},
|
|
188
260
|
};
|
|
189
261
|
}
|
|
262
|
+
/** Probe a fresh Agent before deciding its mode; never treat an unavailable backend as a sandboxed run. */
|
|
263
|
+
export function probeLocalSandbox(project, folder, native) {
|
|
264
|
+
try {
|
|
265
|
+
const controls = createLocalSandboxControls({
|
|
266
|
+
project,
|
|
267
|
+
folder,
|
|
268
|
+
mode: () => "workspace",
|
|
269
|
+
native,
|
|
270
|
+
});
|
|
271
|
+
controls.close();
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
catch (cause) {
|
|
275
|
+
if (cause instanceof LocalSandboxUnavailable)
|
|
276
|
+
return cause.reason;
|
|
277
|
+
throw cause;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type LinuxSandboxProfile } from "./private-local-sandbox-policy.js";
|
|
2
|
+
export declare class UnsafeSeatbeltFolder extends Error {
|
|
3
|
+
readonly name = "UnsafeSeatbeltFolder";
|
|
4
|
+
}
|
|
2
5
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
6
|
+
* Containment supplies the single command-time profile (including HOME-based
|
|
7
|
+
* hidden paths). macOS intentionally grants neither Linux caches nor ssh-agent.
|
|
5
8
|
*/
|
|
6
|
-
export type SeatbeltPolicy =
|
|
7
|
-
temporaryDirectory: string;
|
|
8
|
-
}>;
|
|
9
|
+
export type SeatbeltPolicy = Pick<LinuxSandboxProfile, "mode" | "writableRoot" | "readOnly" | "hidden" | "temporaryDirectory" | "fungiCache">;
|
|
9
10
|
/**
|
|
10
11
|
* Paths are argv definitions, never SBPL source. Both the logical name and its
|
|
11
12
|
* resolved target are protected, including when a protected entry is a symlink.
|
|
@@ -15,10 +16,3 @@ export declare function seatbeltCommand(policy: SeatbeltPolicy, argv: readonly [
|
|
|
15
16
|
args: string[];
|
|
16
17
|
temporaryDirectory: string;
|
|
17
18
|
};
|
|
18
|
-
/** A real startup probe; failure is data, never permission to execute bare. */
|
|
19
|
-
export declare function seatbeltReadiness(policy: SeatbeltPolicy): Readonly<{
|
|
20
|
-
ready: true;
|
|
21
|
-
}> | Readonly<{
|
|
22
|
-
ready: false;
|
|
23
|
-
reason: string;
|
|
24
|
-
}>;
|
|
@@ -1,36 +1,75 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
1
|
import { readFileSync, realpathSync } from "node:fs";
|
|
2
|
+
import { dirname, isAbsolute, relative, sep } from "node:path";
|
|
3
3
|
import { protectedSandboxPaths, } from "./private-local-sandbox-policy.js";
|
|
4
4
|
const sandboxExecutable = "/usr/bin/sandbox-exec";
|
|
5
|
+
export class UnsafeSeatbeltFolder extends Error {
|
|
6
|
+
name = "UnsafeSeatbeltFolder";
|
|
7
|
+
}
|
|
8
|
+
function covers(root, path) {
|
|
9
|
+
const remainder = relative(root, path);
|
|
10
|
+
return (!isAbsolute(remainder) &&
|
|
11
|
+
remainder !== ".." &&
|
|
12
|
+
!remainder.startsWith(`..${sep}`));
|
|
13
|
+
}
|
|
5
14
|
/**
|
|
6
15
|
* Paths are argv definitions, never SBPL source. Both the logical name and its
|
|
7
16
|
* resolved target are protected, including when a protected entry is a symlink.
|
|
8
17
|
*/
|
|
9
18
|
export function seatbeltCommand(policy, argv) {
|
|
19
|
+
const hidden = protectedSandboxPaths(policy.hidden);
|
|
20
|
+
const roots = policy.mode === "workspace"
|
|
21
|
+
? [policy.writableRoot, policy.temporaryDirectory]
|
|
22
|
+
: [policy.temporaryDirectory];
|
|
23
|
+
// Denying writes on the hidden directory alone cannot prevent renaming its
|
|
24
|
+
// parent into a writable root, then reading its credentials at the new path.
|
|
25
|
+
if (roots.some((root) => hidden.some((path) => covers(root, dirname(path)) || covers(path, root))))
|
|
26
|
+
throw new UnsafeSeatbeltFolder("Seatbelt writable folder or temp contains a Fungi hidden record or its parent.");
|
|
10
27
|
const definitions = [];
|
|
11
28
|
const parameter = (name, path) => {
|
|
12
29
|
definitions.push("-D", `${name}=${path}`);
|
|
13
30
|
return `(param "${name}")`;
|
|
14
31
|
};
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
32
|
+
const hiddenRules = hidden.flatMap((path, index) => {
|
|
33
|
+
const value = parameter(`HIDDEN_${String(index)}`, path);
|
|
34
|
+
return [
|
|
35
|
+
`(deny file-read* (literal ${value}))`,
|
|
36
|
+
`(deny file-read* (subpath ${value}))`,
|
|
37
|
+
];
|
|
38
|
+
});
|
|
39
|
+
const hiddenWriteRules = hidden.flatMap((_, index) => {
|
|
40
|
+
const value = `(param "HIDDEN_${String(index)}")`;
|
|
18
41
|
return [
|
|
19
|
-
`(
|
|
20
|
-
`(
|
|
42
|
+
`(deny file-write* (literal ${value}))`,
|
|
43
|
+
`(deny file-write* (subpath ${value}))`,
|
|
21
44
|
];
|
|
22
45
|
});
|
|
23
|
-
const
|
|
46
|
+
const excluded = protectedSandboxPaths(policy.readOnly);
|
|
47
|
+
const carveouts = excluded.map((path, index) => {
|
|
48
|
+
const value = parameter(`PROTECTED_${String(index)}`, path);
|
|
49
|
+
return {
|
|
50
|
+
path,
|
|
51
|
+
rules: [
|
|
52
|
+
`(require-not (literal ${value}))`,
|
|
53
|
+
`(require-not (subpath ${value}))`,
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
const ownTemp = realpathSync(policy.temporaryDirectory);
|
|
58
|
+
// Workspace's cache carve-out protects native receipts under a writable
|
|
59
|
+
// cache parent. It must not cancel the narrower grant for our own temp.
|
|
60
|
+
const cachePaths = policy.fungiCache !== undefined && covers(policy.fungiCache, ownTemp)
|
|
61
|
+
? new Set(protectedSandboxPaths([policy.fungiCache]))
|
|
62
|
+
: undefined;
|
|
24
63
|
const grants = roots.map((path, index) => {
|
|
25
64
|
const value = parameter(`WRITABLE_ROOT_${String(index)}`, realpathSync(path));
|
|
26
|
-
|
|
65
|
+
const protectedRules = carveouts
|
|
66
|
+
.filter(({ path }) => !(policy.mode === "workspace" &&
|
|
67
|
+
index === 1 &&
|
|
68
|
+
cachePaths?.has(path)))
|
|
69
|
+
.flatMap(({ rules }) => rules);
|
|
70
|
+
return `(require-all (subpath ${value}) ${protectedRules.join(" ")})`;
|
|
27
71
|
});
|
|
28
|
-
const anchors = [
|
|
29
|
-
...new Set([
|
|
30
|
-
...roots.map((path) => realpathSync(path)),
|
|
31
|
-
...protectedSandboxPaths(policy.immovable),
|
|
32
|
-
]),
|
|
33
|
-
].map((path, index) => {
|
|
72
|
+
const anchors = [...new Set(roots.map((path) => realpathSync(path)))].map((path, index) => {
|
|
34
73
|
const value = parameter(`ANCHOR_${String(index)}`, path);
|
|
35
74
|
return `(deny file-write-unlink (require-all (vnode-type DIRECTORY) (literal ${value})))`;
|
|
36
75
|
});
|
|
@@ -38,7 +77,9 @@ export function seatbeltCommand(policy, argv) {
|
|
|
38
77
|
const profile = [
|
|
39
78
|
base,
|
|
40
79
|
"(allow file-read*)",
|
|
80
|
+
...hiddenRules,
|
|
41
81
|
`(allow file-write* ${grants.join(" ")})`,
|
|
82
|
+
...hiddenWriteRules,
|
|
42
83
|
...anchors,
|
|
43
84
|
"(deny system-fcntl (fcntl-command 80 110))",
|
|
44
85
|
].join("\n");
|
|
@@ -48,21 +89,3 @@ export function seatbeltCommand(policy, argv) {
|
|
|
48
89
|
temporaryDirectory: realpathSync(policy.temporaryDirectory),
|
|
49
90
|
};
|
|
50
91
|
}
|
|
51
|
-
/** A real startup probe; failure is data, never permission to execute bare. */
|
|
52
|
-
export function seatbeltReadiness(policy) {
|
|
53
|
-
const command = seatbeltCommand(policy, ["/usr/bin/true"]);
|
|
54
|
-
const result = spawnSync(command.executable, command.args, {
|
|
55
|
-
env: { TMPDIR: command.temporaryDirectory },
|
|
56
|
-
encoding: "utf8",
|
|
57
|
-
timeout: 5_000,
|
|
58
|
-
});
|
|
59
|
-
const diagnostic = result.error?.message ?? result.stderr.trim();
|
|
60
|
-
return result.status === 0
|
|
61
|
-
? { ready: true }
|
|
62
|
-
: {
|
|
63
|
-
ready: false,
|
|
64
|
-
reason: diagnostic.length > 0
|
|
65
|
-
? diagnostic
|
|
66
|
-
: `sandbox-exec exited ${String(result.status)} (${String(result.signal)})`,
|
|
67
|
-
};
|
|
68
|
-
}
|
|
@@ -8,6 +8,7 @@ import type { PrivateLocalEffectiveModel } from "./private-local-effective-model
|
|
|
8
8
|
import type { StopAllResult } from "./private-local-stop-all.js";
|
|
9
9
|
import type { ServiceDiagnosticsStatus } from "./private-local-service-log.js";
|
|
10
10
|
import type { LocalSandboxStatus } from "./private-local-sandbox.js";
|
|
11
|
+
import type { LocalPermissionMode } from "./private-local-permission-mode.js";
|
|
11
12
|
import type { LocalUiAttachmentLifetime } from "./private-local-ui-attachment.js";
|
|
12
13
|
/** Live health, not the on-disk identity record or a cached connection snapshot. */
|
|
13
14
|
export type PrivateLocalServiceStatus = Readonly<{
|
|
@@ -36,8 +37,8 @@ export type PrivateLocalServiceSelection = Readonly<{
|
|
|
36
37
|
/** TUI/form watches own their attachment; ACP retains it until connection close. */
|
|
37
38
|
uiAttachmentLifetime?: LocalUiAttachmentLifetime | undefined;
|
|
38
39
|
clientPath?: string | undefined;
|
|
39
|
-
/** Person-selected
|
|
40
|
-
|
|
40
|
+
/** Person-selected Agent mode, not an approval setting. */
|
|
41
|
+
mode?: LocalPermissionMode | undefined;
|
|
41
42
|
databasePath?: string | undefined;
|
|
42
43
|
sessionId?: string | undefined;
|
|
43
44
|
fungiMcp?: PrivateFungiMcpSelection | undefined;
|
|
@@ -46,7 +47,7 @@ export type PrivateLocalSessionCreation = Readonly<{
|
|
|
46
47
|
invocationRoot: string;
|
|
47
48
|
uiAttachmentLifetime?: LocalUiAttachmentLifetime | undefined;
|
|
48
49
|
clientPath?: string | undefined;
|
|
49
|
-
|
|
50
|
+
mode?: LocalPermissionMode | undefined;
|
|
50
51
|
databasePath: string;
|
|
51
52
|
requestId: string;
|
|
52
53
|
fungiMcp?: PrivateFungiMcpSelection | undefined;
|
|
@@ -111,7 +112,7 @@ export type PrivateLocalSessionChoice = Readonly<{
|
|
|
111
112
|
/** Reopen this saved store through the persistent service connection. */
|
|
112
113
|
resume: (options?: PrivateLocalServiceRequestOptions & {
|
|
113
114
|
fungiMcp?: PrivateFungiMcpSelection | undefined;
|
|
114
|
-
|
|
115
|
+
mode?: LocalPermissionMode | undefined;
|
|
115
116
|
}) => Promise<PrivateLocalServiceConnection>;
|
|
116
117
|
}>;
|
|
117
118
|
export type PrivateLocalSessionProblem = Readonly<{
|