@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.
Files changed (68) hide show
  1. package/dist/private-files-patch.js +3 -17
  2. package/dist/private-invocation-computer.d.ts +224 -12
  3. package/dist/private-invocation-computer.js +162 -22
  4. package/dist/private-invocation-files.d.ts +3 -2
  5. package/dist/private-invocation-files.js +22 -3
  6. package/dist/private-local-agent-control.d.ts +2 -2
  7. package/dist/private-local-bwrap.d.ts +9 -7
  8. package/dist/private-local-bwrap.js +26 -21
  9. package/dist/private-local-computer-processes.d.ts +15 -0
  10. package/dist/private-local-computer-processes.js +13 -0
  11. package/dist/private-local-containment.d.ts +15 -13
  12. package/dist/private-local-containment.js +49 -65
  13. package/dist/private-local-delegation.d.ts +3 -3
  14. package/dist/private-local-delegation.js +32 -19
  15. package/dist/private-local-exec-review.d.ts +10 -0
  16. package/dist/private-local-exec-review.js +50 -0
  17. package/dist/private-local-exec-rules.d.ts +14 -0
  18. package/dist/private-local-exec-rules.js +420 -0
  19. package/dist/private-local-git-exclude.js +25 -15
  20. package/dist/private-local-linux-sandbox.d.ts +5 -5
  21. package/dist/private-local-linux-sandbox.js +63 -30
  22. package/dist/private-local-mail.d.ts +2 -2
  23. package/dist/private-local-mesh-links.d.ts +4 -1
  24. package/dist/private-local-mesh-links.js +50 -3
  25. package/dist/private-local-native-digests.js +1 -1
  26. package/dist/private-local-native-install-error.d.ts +2 -0
  27. package/dist/private-local-native-install-error.js +10 -0
  28. package/dist/private-local-native-staging.d.ts +7 -8
  29. package/dist/private-local-native-staging.js +132 -43
  30. package/dist/private-local-permission-mode.d.ts +10 -0
  31. package/dist/private-local-permission-mode.js +18 -0
  32. package/dist/private-local-runtime-host.d.ts +19 -5
  33. package/dist/private-local-runtime-host.js +378 -65
  34. package/dist/private-local-sandbox-policy.d.ts +30 -11
  35. package/dist/private-local-sandbox-policy.js +223 -373
  36. package/dist/private-local-sandbox.d.ts +30 -25
  37. package/dist/private-local-sandbox.js +190 -100
  38. package/dist/private-local-seatbelt.d.ts +7 -13
  39. package/dist/private-local-seatbelt.js +55 -32
  40. package/dist/private-local-service-contract.d.ts +5 -4
  41. package/dist/private-local-service-discovery.d.ts +19 -7
  42. package/dist/private-local-service-discovery.js +4 -3
  43. package/dist/private-local-service-listener.js +40 -2
  44. package/dist/private-local-service-log.d.ts +8 -0
  45. package/dist/private-local-service-log.js +3 -0
  46. package/dist/private-local-service-owner.d.ts +18 -10
  47. package/dist/private-local-service-owner.js +117 -25
  48. package/dist/private-local-service.js +30 -3
  49. package/dist/private-local-session-discovery.d.ts +2 -1
  50. package/dist/private-local-session-discovery.js +18 -6
  51. package/dist/private-mcp-form-contract.d.ts +8 -1
  52. package/dist/private-mcp-form-contract.js +8 -1
  53. package/dist/private-mcp-form-session.d.ts +33 -3
  54. package/dist/private-mcp-form-session.js +28 -5
  55. package/dist/private-oyster-mcp-auth.d.ts +1 -1
  56. package/dist/private-oyster-mcp-auth.js +139 -7
  57. package/dist/private-provider-rejection.js +1 -1
  58. package/dist/seatbelt/safe-base.sbpl +2 -2
  59. package/native/bin/bwrap-build-source/Dockerfile +2 -0
  60. package/native/bin/bwrap-build-source/README.md +6 -6
  61. package/native/bin/bwrap-build-source/pin.mjs +2 -2
  62. package/native/bin/bwrap-build-source/record.mjs +12 -1
  63. package/native/bin/bwrap-build-source/vendor/seccomp-unix-block.c +10 -7
  64. package/native/bin/bwrap-build.json +3 -2
  65. package/native/bin/fungi-apply-patch +0 -0
  66. package/native/bin/fungi-seccomp-workspace.bpf +0 -0
  67. package/native/bin/fungi-seccomp-workspace.bpf.sha256 +1 -0
  68. 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 { type NativeRunLivenessProbe } from "./private-local-sandbox-policy.js";
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.ZodDiscriminatedUnion<[z.ZodObject<{
7
- state: z.ZodLiteral<"available">;
8
- requireSandbox: z.ZodBoolean;
9
- }, z.core.$strict>, z.ZodObject<{
10
- state: z.ZodLiteral<"unavailable">;
11
- requireSandbox: z.ZodBoolean;
12
- reason: z.ZodObject<{
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
- missing: "missing";
15
- "missing-artifact": "missing-artifact";
16
- "invalid-digest": "invalid-digest";
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>], "state">;
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 requirement; the adapter owns only preparation.
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
- required(): boolean;
45
+ agentId?: string;
46
+ mode(): LocalPermissionMode;
47
+ defaultReason?: z.output<typeof sandboxUnavailableReasonSchema> | undefined;
45
48
  platform?: string;
46
49
  readNativePatchSource?: NativeSourceReader;
47
- probeNativeRunLiveness?: NativeRunLivenessProbe;
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.discriminatedUnion("state", [
10
- z.strictObject({
11
- state: z.literal("available"),
12
- requireSandbox: z.boolean(),
13
- }),
14
- z.strictObject({
15
- state: z.literal("unavailable"),
16
- requireSandbox: z.boolean(),
17
- reason: sandboxUnavailableReasonSchema,
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 requirement; the adapter owns only preparation.
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 required = input.required();
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, independent of the optional/required setting.
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
- const launcher = process.env.FUNGI_LAUNCHER;
36
- const cli = launcher === undefined
37
- ? fileURLToPath(import.meta.url)
38
- : process.env.FUNGI_BUNDLED_CLI;
39
- if (cli === undefined)
40
- throw new UnsafeHostCode("Packed CLI entry was not provided by the launcher; reinstall Fungi outside the project.");
41
- assertHostCodeOutsideAgent(input.project, cli, launcher ?? cli);
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
- throw new LocalSandboxUnavailable(cause instanceof UnsafeHostCode
45
- ? sandboxUnavailableReason(cause)
46
- : {
47
- kind: "unsafe-host-code",
48
- message: `Fungi host JavaScript identity cannot be verified (${cause instanceof Error ? cause.message : String(cause)}); reinstall Fungi using a copying install outside the project.`,
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
- const platform = input.platform === undefined ? {} : { platform: input.platform };
52
- const native = createNativePayloadStaging(input.project, input.readNativePatchSource, input.probeNativeRunLiveness);
53
- const initial = establishLocalSandbox({
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 (unavailable.kind === "invalid-folder") {
63
- native.close();
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 (required) {
67
- native.close();
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: "invalid-folder",
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 (sandbox === undefined) {
94
- const admission = establishLocalSandbox({
95
- project: input.project,
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 === "invalid-folder" && !everReady)
202
+ if (unavailable.kind === "folder")
123
203
  throw new Error(unavailable.message);
124
- // A formerly ready sandbox never becomes optional bare execution.
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
- assertWrite: (path) => {
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
- if (!contains(policy.writableRoot, target) ||
162
- protectedSandboxPaths(policy.readOnly).some((protectedPath) => contains(protectedPath, target)) ||
163
- protectedSandboxPaths(policy.immovable).includes(target))
164
- throw new Error("Files write targets a protected Local path");
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
- ? { state: "available", requireSandbox: input.required() }
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.close();
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 LocalSandboxPolicy } from "./private-local-sandbox-policy.js";
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
- * The containment owner supplies temporary-directory lifetime and command-time
4
- * protected paths; this adapter owns only the maintained Seatbelt translation.
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 = LocalSandboxPolicy & Readonly<{
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 excluded = protectedSandboxPaths(policy.readOnly);
16
- const carveouts = excluded.flatMap((path, index) => {
17
- const value = parameter(`PROTECTED_${String(index)}`, path);
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
- `(require-not (literal ${value}))`,
20
- `(require-not (subpath ${value}))`,
42
+ `(deny file-write* (literal ${value}))`,
43
+ `(deny file-write* (subpath ${value}))`,
21
44
  ];
22
45
  });
23
- const roots = [policy.writableRoot, policy.temporaryDirectory];
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
- return `(require-all (subpath ${value}) ${carveouts.join(" ")})`;
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 sandbox requirement, not an approval setting. */
40
- requireSandbox?: boolean | undefined;
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
- requireSandbox?: boolean | undefined;
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
- requireSandbox?: boolean | undefined;
115
+ mode?: LocalPermissionMode | undefined;
115
116
  }) => Promise<PrivateLocalServiceConnection>;
116
117
  }>;
117
118
  export type PrivateLocalSessionProblem = Readonly<{