@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.
Files changed (68) hide show
  1. package/dist/private-files-patch.js +39 -36
  2. package/dist/private-invocation-computer.d.ts +224 -5
  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 +17 -7
  8. package/dist/private-local-bwrap.js +73 -33
  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 +18 -8
  12. package/dist/private-local-containment.js +70 -45
  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 -3
  21. package/dist/private-local-linux-sandbox.js +71 -25
  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 +2 -0
  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 +16 -0
  29. package/dist/private-local-native-staging.js +138 -0
  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 +391 -71
  34. package/dist/private-local-sandbox-policy.d.ts +46 -6
  35. package/dist/private-local-sandbox-policy.js +350 -71
  36. package/dist/private-local-sandbox.d.ts +34 -18
  37. package/dist/private-local-sandbox.js +225 -60
  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 +18 -6
  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 +43 -9
  49. package/dist/private-local-session-discovery.d.ts +3 -2
  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 +3 -1
  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 +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.discriminatedUnion("state", [
7
- z.strictObject({
8
- state: z.literal("available"),
9
- requireSandbox: z.boolean(),
10
- }),
11
- z.strictObject({
12
- state: z.literal("unavailable"),
13
- requireSandbox: z.boolean(),
14
- reason: sandboxUnavailableReasonSchema,
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 requirement; the adapter owns only preparation.
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 required = input.required();
32
+ const mode = input.mode();
27
33
  const folder = resolveAgentFolder(input.project, input.folder);
28
- const initial = establishLocalSandbox({
29
- project: input.project,
30
- folder: folder.absolute,
31
- });
32
- let sandbox = initial.state === "ready" ? initial.sandbox : undefined;
33
- let unavailable = initial.state === "unavailable" ? initial.reason : undefined;
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 (unavailable.kind === "invalid-folder")
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
- if (required)
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
- const current = resolveAgentFolder(input.project, input.folder);
45
- if (current.absolute !== folder.absolute)
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
- const launch = (cwd, argv) => {
49
- assertFolder();
50
- if (sandbox === undefined) {
51
- const admission = establishLocalSandbox({
52
- project: input.project,
53
- folder: folder.absolute,
54
- });
55
- if (admission.state === "ready")
56
- sandbox = admission.sandbox;
57
- else
58
- unavailable = admission.reason;
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
- if (!(cause instanceof LocalSandboxUnavailable))
68
- throw cause;
69
- unavailable = cause.reason;
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 === "invalid-folder")
202
+ if (unavailable.kind === "folder")
75
203
  throw new Error(unavailable.message);
76
- if (input.required())
77
- throw new LocalSandboxUnavailable(unavailable);
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
- 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;
88
226
  assertReady();
89
- const target = resolve(path);
90
227
  const policy = sandboxPolicy(input.project, folder.absolute);
91
- if (!contains(policy.writableRoot, target) ||
92
- protectedSandboxPaths(policy.readOnly).some((protectedPath) => contains(protectedPath, target)) ||
93
- protectedSandboxPaths(policy.immovable).includes(target))
94
- 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
+ }
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
- ? { state: "available", requireSandbox: input.required() }
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
- sandbox?.close();
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 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<{
@@ -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 = 3;
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<3>;
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<3>;
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
- requireSandbox: z.ZodOptional<z.ZodBoolean>;
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
- requireSandbox: z.ZodOptional<z.ZodBoolean>;
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 = 3;
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
- requireSandbox: z.boolean().optional(),
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
- requireSandbox: z.boolean().optional(),
77
+ mode: localPermissionModeSchema.optional(),
77
78
  databasePath: z.string().min(1),
78
79
  requestId: z.string().min(1),
79
80
  fungiMcp: fungiMcpSelectionSchema.optional(),