@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
|
@@ -1,114 +1,279 @@
|
|
|
1
|
-
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
1
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { rmSync } from "node:fs";
|
|
4
|
+
import { createNativePayloadStaging } from "./private-local-native-staging.js";
|
|
2
5
|
import { z } from "zod";
|
|
3
|
-
import { establishLocalSandbox, LocalSandboxUnavailable, sandboxUnavailableReasonSchema, } from "./private-local-containment.js";
|
|
4
|
-
import { protectedSandboxPaths, resolveAgentFolder, sandboxPolicy, } from "./private-local-sandbox-policy.js";
|
|
6
|
+
import { establishLocalSandbox, LocalSandboxUnavailable, sandboxUnavailableReason, sandboxUnavailableReasonSchema, } from "./private-local-containment.js";
|
|
7
|
+
import { protectedSandboxPaths, assertHostCodeOutsideAgent, UnsafeHostCode, resolveAgentFolder, sandboxPolicy, agentSandboxTemporaryDirectory, } from "./private-local-sandbox-policy.js";
|
|
5
8
|
/** Availability is not a hostile-program security or an approval status. */
|
|
6
|
-
export const sandboxStatusSchema = z.
|
|
7
|
-
z.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
+
}
|
|
17
23
|
function contains(root, path) {
|
|
18
24
|
const within = relative(root, path);
|
|
19
25
|
return (!isAbsolute(within) && within !== ".." && !within.startsWith(`..${sep}`));
|
|
20
26
|
}
|
|
21
27
|
/**
|
|
22
|
-
* Native Local owns the live
|
|
28
|
+
* Native Local owns the live mode; the adapter owns only preparation.
|
|
23
29
|
* Every consumer calls this authority immediately before launching or writing.
|
|
24
30
|
*/
|
|
25
31
|
export function createLocalSandboxControls(input) {
|
|
26
|
-
const
|
|
32
|
+
const mode = input.mode();
|
|
27
33
|
const folder = resolveAgentFolder(input.project, input.folder);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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)}`;
|
|
38
|
+
// The packed Node wrapper passes the paths it actually loaded. In source
|
|
39
|
+
// runs this module is the executing host authority instead. Check before
|
|
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;
|
|
88
|
+
try {
|
|
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
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch (cause) {
|
|
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;
|
|
113
|
+
}
|
|
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;
|
|
34
117
|
if (unavailable !== undefined) {
|
|
35
|
-
if (
|
|
118
|
+
if (temporaryDirectory !== undefined)
|
|
119
|
+
rmSync(temporaryDirectory, { recursive: true, force: true });
|
|
120
|
+
if (unavailable.kind === "folder") {
|
|
121
|
+
if (input.native === undefined)
|
|
122
|
+
native?.close();
|
|
36
123
|
throw new Error(unavailable.message);
|
|
37
|
-
|
|
124
|
+
}
|
|
125
|
+
if (mode !== "full-access") {
|
|
126
|
+
if (input.native === undefined)
|
|
127
|
+
native?.close();
|
|
38
128
|
throw new LocalSandboxUnavailable(unavailable);
|
|
129
|
+
}
|
|
39
130
|
}
|
|
40
131
|
let closed = false;
|
|
41
132
|
const assertFolder = () => {
|
|
42
133
|
if (closed)
|
|
43
134
|
throw new Error("Local sandbox owner is closed");
|
|
44
|
-
|
|
45
|
-
|
|
135
|
+
try {
|
|
136
|
+
const current = resolveAgentFolder(input.project, input.folder);
|
|
137
|
+
if (current.absolute === folder.absolute)
|
|
138
|
+
return;
|
|
46
139
|
throw new Error("Local Agent folder authority changed while open");
|
|
140
|
+
}
|
|
141
|
+
catch (cause) {
|
|
142
|
+
unavailable = {
|
|
143
|
+
kind: "folder",
|
|
144
|
+
message: cause instanceof Error ? cause.message : String(cause),
|
|
145
|
+
};
|
|
146
|
+
throw new LocalSandboxUnavailable(unavailable);
|
|
147
|
+
}
|
|
47
148
|
};
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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);
|
|
59
179
|
}
|
|
180
|
+
}
|
|
181
|
+
function launch(cwd, argv, readOnlyHelperFile) {
|
|
182
|
+
assertFolder();
|
|
183
|
+
if (input.mode() === "full-access")
|
|
184
|
+
return undefined;
|
|
185
|
+
prepareSandbox(input.mode() === "read-only" ? "read-only" : "workspace");
|
|
60
186
|
if (sandbox !== undefined) {
|
|
61
187
|
try {
|
|
62
|
-
const command = sandbox.command(cwd, argv);
|
|
188
|
+
const command = sandbox.command(cwd, argv, readOnlyHelperFile);
|
|
63
189
|
unavailable = undefined;
|
|
64
190
|
return command;
|
|
65
191
|
}
|
|
66
192
|
catch (cause) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
193
|
+
unavailable =
|
|
194
|
+
cause instanceof LocalSandboxUnavailable
|
|
195
|
+
? cause.reason
|
|
196
|
+
: sandboxUnavailableReason(cause);
|
|
197
|
+
throw new LocalSandboxUnavailable(unavailable);
|
|
70
198
|
}
|
|
71
199
|
}
|
|
72
200
|
if (unavailable === undefined)
|
|
73
201
|
throw new Error("Local sandbox preparation produced no result");
|
|
74
|
-
if (unavailable.kind === "
|
|
202
|
+
if (unavailable.kind === "folder")
|
|
75
203
|
throw new Error(unavailable.message);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
// No executable has started. Only this owner admits the bare fallback.
|
|
79
|
-
return undefined;
|
|
80
|
-
};
|
|
204
|
+
throw new LocalSandboxUnavailable(unavailable);
|
|
205
|
+
}
|
|
81
206
|
const assertReady = () => {
|
|
207
|
+
if (input.mode() === "full-access")
|
|
208
|
+
return;
|
|
82
209
|
launch(folder.absolute, ["/usr/bin/true"]);
|
|
83
210
|
};
|
|
84
211
|
return {
|
|
85
212
|
launch,
|
|
86
213
|
assertReady,
|
|
87
|
-
|
|
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;
|
|
88
226
|
assertReady();
|
|
89
|
-
const target = resolve(path);
|
|
90
227
|
const policy = sandboxPolicy(input.project, folder.absolute);
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
+
}
|
|
95
238
|
},
|
|
96
239
|
status: () => {
|
|
97
240
|
if (closed)
|
|
98
241
|
throw new Error("Local sandbox owner is closed");
|
|
99
242
|
return unavailable === undefined
|
|
100
|
-
? {
|
|
101
|
-
: {
|
|
102
|
-
state: "unavailable",
|
|
103
|
-
requireSandbox: input.required(),
|
|
104
|
-
reason: unavailable,
|
|
105
|
-
};
|
|
243
|
+
? { mode: input.mode(), available: true }
|
|
244
|
+
: { mode: input.mode(), available: false, reason: unavailable };
|
|
106
245
|
},
|
|
107
246
|
close: () => {
|
|
108
247
|
if (closed)
|
|
109
248
|
return;
|
|
110
249
|
closed = true;
|
|
111
|
-
|
|
250
|
+
try {
|
|
251
|
+
sandbox?.close();
|
|
252
|
+
}
|
|
253
|
+
finally {
|
|
254
|
+
if (input.native === undefined)
|
|
255
|
+
native?.close();
|
|
256
|
+
if (temporaryDirectory !== undefined)
|
|
257
|
+
rmSync(temporaryDirectory, { recursive: true });
|
|
258
|
+
}
|
|
112
259
|
},
|
|
113
260
|
};
|
|
114
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<{
|
|
@@ -4,7 +4,7 @@ export { sandboxStatusSchema } from "./private-local-sandbox.js";
|
|
|
4
4
|
* The client-service wire contract. Bump it with any change an older client
|
|
5
5
|
* cannot read; a running client then asks its window to restart.
|
|
6
6
|
*/
|
|
7
|
-
export declare const privateLocalServiceProtocol =
|
|
7
|
+
export declare const privateLocalServiceProtocol = 4;
|
|
8
8
|
/** This build's identity: the packed CLI's content hash, or `source` for a source run. */
|
|
9
9
|
export declare function privateLocalServiceVersion(): string;
|
|
10
10
|
/**
|
|
@@ -19,7 +19,7 @@ declare const serviceLifecycleSchema: z.ZodObject<{
|
|
|
19
19
|
startedAt: z.ZodString;
|
|
20
20
|
}, z.core.$loose>;
|
|
21
21
|
declare const serviceRecordSchema: z.ZodObject<{
|
|
22
|
-
protocol: z.ZodLiteral<
|
|
22
|
+
protocol: z.ZodLiteral<4>;
|
|
23
23
|
baseUrl: z.ZodURL;
|
|
24
24
|
version: z.ZodString;
|
|
25
25
|
serviceId: z.ZodString & z.ZodType<import("@fungi.computer/cairn").CairnId<"local-service", "service", 1>, string, z.core.$ZodTypeInternals<import("@fungi.computer/cairn").CairnId<"local-service", "service", 1>, string>>;
|
|
@@ -29,7 +29,7 @@ declare const serviceRecordSchema: z.ZodObject<{
|
|
|
29
29
|
export type ServiceRecord = z.infer<typeof serviceRecordSchema>;
|
|
30
30
|
/** Mandatory live diagnostics; never written into the registration pointer. */
|
|
31
31
|
export declare const liveServiceStatusSchema: z.ZodObject<{
|
|
32
|
-
protocol: z.ZodLiteral<
|
|
32
|
+
protocol: z.ZodLiteral<4>;
|
|
33
33
|
baseUrl: z.ZodURL;
|
|
34
34
|
version: z.ZodString;
|
|
35
35
|
serviceId: z.ZodString & z.ZodType<import("@fungi.computer/cairn").CairnId<"local-service", "service", 1>, string, z.core.$ZodTypeInternals<import("@fungi.computer/cairn").CairnId<"local-service", "service", 1>, string>>;
|
|
@@ -76,7 +76,11 @@ export declare const selectionSchema: z.ZodObject<{
|
|
|
76
76
|
}>>;
|
|
77
77
|
invocationRoot: z.ZodString;
|
|
78
78
|
clientPath: z.ZodString;
|
|
79
|
-
|
|
79
|
+
mode: z.ZodOptional<z.ZodEnum<{
|
|
80
|
+
"read-only": "read-only";
|
|
81
|
+
workspace: "workspace";
|
|
82
|
+
"full-access": "full-access";
|
|
83
|
+
}>>;
|
|
80
84
|
databasePath: z.ZodOptional<z.ZodString>;
|
|
81
85
|
sessionId: z.ZodOptional<z.ZodString>;
|
|
82
86
|
fungiMcp: z.ZodOptional<z.ZodObject<{
|
|
@@ -91,7 +95,11 @@ export declare const sessionCreationSchema: z.ZodObject<{
|
|
|
91
95
|
}>>;
|
|
92
96
|
invocationRoot: z.ZodString;
|
|
93
97
|
clientPath: z.ZodString;
|
|
94
|
-
|
|
98
|
+
mode: z.ZodOptional<z.ZodEnum<{
|
|
99
|
+
"read-only": "read-only";
|
|
100
|
+
workspace: "workspace";
|
|
101
|
+
"full-access": "full-access";
|
|
102
|
+
}>>;
|
|
95
103
|
databasePath: z.ZodString;
|
|
96
104
|
requestId: z.ZodString;
|
|
97
105
|
fungiMcp: z.ZodOptional<z.ZodObject<{
|
|
@@ -100,13 +108,17 @@ export declare const sessionCreationSchema: z.ZodObject<{
|
|
|
100
108
|
}, z.core.$strict>>;
|
|
101
109
|
}, z.core.$strict>;
|
|
102
110
|
export declare const sessionInspectionSchema: z.ZodObject<{
|
|
111
|
+
mode: z.ZodOptional<z.ZodEnum<{
|
|
112
|
+
"read-only": "read-only";
|
|
113
|
+
workspace: "workspace";
|
|
114
|
+
"full-access": "full-access";
|
|
115
|
+
}>>;
|
|
103
116
|
databasePath: z.ZodOptional<z.ZodString>;
|
|
104
117
|
invocationRoot: z.ZodString;
|
|
105
118
|
fungiMcp: z.ZodOptional<z.ZodObject<{
|
|
106
119
|
endpoint: z.ZodPipe<z.ZodURL, z.ZodString>;
|
|
107
120
|
authentication: z.ZodOptional<z.ZodLiteral<"oyster-key">>;
|
|
108
121
|
}, z.core.$strict>>;
|
|
109
|
-
requireSandbox: z.ZodOptional<z.ZodBoolean>;
|
|
110
122
|
sessionId: z.ZodString;
|
|
111
123
|
limit: z.ZodNumber;
|
|
112
124
|
}, z.core.$strict>;
|
|
@@ -6,6 +6,7 @@ import { join, resolve } from "node:path";
|
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import lockfile from "proper-lockfile";
|
|
8
8
|
import { z } from "zod";
|
|
9
|
+
import { localPermissionModeSchema } from "./private-local-permission-mode.js";
|
|
9
10
|
import { localServiceIds } from "./private-local-service-ids.js";
|
|
10
11
|
import { acquireServiceOwnerLease, readServiceLeaseOwner, } from "./private-local-service-lease.js";
|
|
11
12
|
import { modelReasoningLevels } from "@fungi.computer/shiitake/model-preference";
|
|
@@ -16,7 +17,7 @@ export { sandboxStatusSchema } from "./private-local-sandbox.js";
|
|
|
16
17
|
* The client-service wire contract. Bump it with any change an older client
|
|
17
18
|
* cannot read; a running client then asks its window to restart.
|
|
18
19
|
*/
|
|
19
|
-
export const privateLocalServiceProtocol =
|
|
20
|
+
export const privateLocalServiceProtocol = 4;
|
|
20
21
|
/** This build's identity: the packed CLI's content hash, or `source` for a source run. */
|
|
21
22
|
export function privateLocalServiceVersion() {
|
|
22
23
|
return (z
|
|
@@ -64,7 +65,7 @@ export const selectionSchema = z.strictObject({
|
|
|
64
65
|
uiAttachmentLifetime: localUiAttachmentLifetimeSchema.optional(),
|
|
65
66
|
invocationRoot: z.string().min(1),
|
|
66
67
|
clientPath: privateLocalExecPathSchema,
|
|
67
|
-
|
|
68
|
+
mode: localPermissionModeSchema.optional(),
|
|
68
69
|
databasePath: z.string().min(1).optional(),
|
|
69
70
|
sessionId: z.string().min(1).optional(),
|
|
70
71
|
fungiMcp: fungiMcpSelectionSchema.optional(),
|
|
@@ -73,7 +74,7 @@ export const sessionCreationSchema = z.strictObject({
|
|
|
73
74
|
uiAttachmentLifetime: localUiAttachmentLifetimeSchema.optional(),
|
|
74
75
|
invocationRoot: z.string().min(1),
|
|
75
76
|
clientPath: privateLocalExecPathSchema,
|
|
76
|
-
|
|
77
|
+
mode: localPermissionModeSchema.optional(),
|
|
77
78
|
databasePath: z.string().min(1),
|
|
78
79
|
requestId: z.string().min(1),
|
|
79
80
|
fungiMcp: fungiMcpSelectionSchema.optional(),
|