@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,25 +1,25 @@
1
- import { existsSync, mkdirSync, realpathSync, statSync, writeFileSync, } from "node:fs";
2
- import { isAbsolute, join } from "node:path";
1
+ import { existsSync, mkdirSync, realpathSync, statSync } from "node:fs";
2
+ import { isAbsolute, join, relative, sep } from "node:path";
3
3
  import { protectedSandboxPaths, } from "./private-local-sandbox-policy.js";
4
4
  /** Ordinary upstream bwrap argv. There is no approval or bare-command path. */
5
5
  export function linuxSandboxCommand(policy, payload, cwd, argv, host = process.env, readOnlyHelperFile) {
6
6
  const root = policy.writableRoot;
7
7
  // Bind sources must exist. Only the trusted host prepares the reserved paths.
8
8
  mkdirSync(join(root, ".fungi"), { recursive: true });
9
- const reserved = new Set(policy.readOnly);
10
- for (const git of policy.immovable) {
11
- if (reserved.has(join(git, "hooks")))
12
- mkdirSync(join(git, "hooks"), { recursive: true });
13
- for (const config of ["config", "config.worktree"])
14
- if (reserved.has(join(git, config)) && !existsSync(join(git, config)))
15
- writeFileSync(join(git, config), "", { flag: "wx" });
16
- // Git reads a present commondir and rejects an empty one; "." names the
17
- // Git directory itself, which is what an absent commondir means.
18
- if (!existsSync(join(git, "commondir")))
19
- writeFileSync(join(git, "commondir"), ".", { flag: "wx" });
20
- }
9
+ // A missing carve-out would be skipped by the existing-path ro-bind loop,
10
+ // allowing Workspace to seed self-attested native-patch cache receipts.
11
+ if (policy.fungiCache !== undefined)
12
+ mkdirSync(policy.fungiCache, { recursive: true, mode: 0o700 });
21
13
  const args = [
22
- "--unshare-all",
14
+ ...(policy.mode === "read-only"
15
+ ? ["--unshare-all"]
16
+ : [
17
+ "--unshare-user",
18
+ "--unshare-pid",
19
+ "--unshare-ipc",
20
+ "--unshare-uts",
21
+ "--unshare-cgroup-try",
22
+ ]),
23
23
  "--die-with-parent",
24
24
  "--new-session",
25
25
  "--seccomp",
@@ -28,27 +28,43 @@ export function linuxSandboxCommand(policy, payload, cwd, argv, host = process.e
28
28
  "/",
29
29
  "/",
30
30
  // Private system mounts precede the folder bind so /tmp projects survive.
31
- "--tmpfs",
31
+ "--bind",
32
+ policy.temporaryDirectory,
32
33
  "/tmp",
33
34
  "--tmpfs",
34
35
  "/run",
36
+ ...(existsSync("/var/run") && realpathSync("/var/run") !== "/run"
37
+ ? ["--tmpfs", realpathSync("/var/run")]
38
+ : []),
35
39
  "--dev",
36
40
  "/dev",
37
41
  "--proc",
38
42
  "/proc",
39
- "--bind",
40
- root,
41
- root,
42
43
  ];
43
- for (const path of policy.immovable)
44
+ // A selected folder under /tmp disappears behind the private /tmp bind.
45
+ // Restore it read-only for Read-only; Workspace will bind it writable below.
46
+ if (policy.mode === "read-only")
47
+ args.push("--ro-bind", root, root);
48
+ // System masks precede every writable grant. Git is rebound read-only after.
49
+ for (const path of policy.writable)
44
50
  args.push("--bind", path, path);
45
- for (const path of protectedSandboxPaths(policy.readOnly))
46
- args.push("--ro-bind", path, path);
47
- if (existsSync("/var/run") && realpathSync("/var/run") !== "/run")
48
- args.push("--tmpfs", realpathSync("/var/run"));
51
+ // External Git metadata is never a writable grant. Missing paths do not
52
+ // become host-side bwrap mountpoints.
53
+ if (policy.mode === "workspace")
54
+ for (const path of protectedSandboxPaths(policy.readOnly))
55
+ if (existsSync(path))
56
+ args.push("--ro-bind", path, path);
49
57
  const runtime = host.XDG_RUNTIME_DIR;
50
58
  if (runtime && isAbsolute(runtime) && existsSync(runtime))
51
59
  args.push("--tmpfs", realpathSync(runtime));
60
+ // systemd's /etc/resolv.conf is often a symlink into the masked /run tree.
61
+ // Restore only its target file in networked Workspace, never the resolver's
62
+ // runtime directory or user bus.
63
+ if (policy.mode === "workspace" && existsSync("/etc/resolv.conf")) {
64
+ const resolver = realpathSync("/etc/resolv.conf");
65
+ if (resolver.startsWith("/run/"))
66
+ args.push("--ro-bind", resolver, resolver);
67
+ }
52
68
  // Stopgap masks, not an inventory of all possible host IPC. /run also
53
69
  // covers the usual /var/run alias. These follow folder/protected binds so
54
70
  // a selected socket inside the project is not exposed again by a later bind.
@@ -62,12 +78,23 @@ export function linuxSandboxCommand(policy, payload, cwd, argv, host = process.e
62
78
  if (existsSync(path) && statSync(path).isDirectory())
63
79
  args.push("--tmpfs", realpathSync(path));
64
80
  }
65
- const agentSocket = host.SSH_AUTH_SOCK;
66
- if (agentSocket &&
67
- isAbsolute(agentSocket) &&
68
- existsSync(agentSocket) &&
69
- statSync(agentSocket).isSocket())
70
- args.push("--ro-bind", "/dev/null", realpathSync(agentSocket));
81
+ for (const path of policy.hidden) {
82
+ // Host /tmp is already replaced. An external fixture HOME beneath it has
83
+ // no mountpoint in the guest; restoring it would reveal host data.
84
+ const underTmp = relative("/tmp", path);
85
+ const underRoot = relative(root, path);
86
+ if (underTmp !== ".." &&
87
+ !underTmp.startsWith(`..${sep}`) &&
88
+ (underRoot === ".." || underRoot.startsWith(`..${sep}`)))
89
+ continue;
90
+ // Absent records cannot be read; never create a user's credential paths.
91
+ if (existsSync(path) && statSync(path).isDirectory())
92
+ args.push("--tmpfs", path);
93
+ }
94
+ if (policy.sshAgentSocket !== undefined)
95
+ args.push("--bind", policy.sshAgentSocket, policy.sshAgentSocket);
96
+ if (policy.sshAgentMask !== undefined)
97
+ args.push("--ro-bind", "/dev/null", policy.sshAgentMask);
71
98
  // Native Files passes its resolved helper, never an untrusted exec argv[0].
72
99
  // Re-expose only that file after private mounts; its siblings stay hidden.
73
100
  if (readOnlyHelperFile !== undefined) {
@@ -75,6 +102,12 @@ export function linuxSandboxCommand(policy, payload, cwd, argv, host = process.e
75
102
  throw new Error("Local native helper must be a regular file");
76
103
  args.push("--ro-bind", readOnlyHelperFile, readOnlyHelperFile);
77
104
  }
105
+ // R is a bind mount: let sandboxed Git reads discover a parent repository
106
+ // when the selected folder has no .git of its own.
107
+ args.push("--setenv", "GIT_DISCOVERY_ACROSS_FILESYSTEM", "1");
108
+ args.push("--setenv", "FUNGI_SANDBOX", policy.mode);
109
+ if (policy.mode === "read-only")
110
+ args.push("--setenv", "FUNGI_SANDBOX_NETWORK_DISABLED", "1");
78
111
  args.push("--chdir", cwd, "--", ...argv);
79
112
  return { ...payload, args };
80
113
  }
@@ -1,10 +1,10 @@
1
1
  import type { BirdDogMail } from "@fungi.computer/bird-dog";
2
2
  import { type BirdDogEffect } from "@fungi.computer/bird-dog/effect";
3
3
  import type { PrivateSqliteDatabase } from "./private-sqlite-owner.js";
4
- import type { Accepted } from "@fungi.computer/shiitake";
4
+ import type { DelegationReceipt } from "@fungi.computer/shiitake";
5
5
  export type PrivateLocalMailOptions = Readonly<{
6
6
  /** Admit one reply at its addressed Agent; a rejection retries later. */
7
- deliver: (mail: BirdDogMail) => Promise<Accepted>;
7
+ deliver: (mail: BirdDogMail) => Promise<DelegationReceipt>;
8
8
  scheduleWake?: (at: number) => void;
9
9
  now?: () => number;
10
10
  }>;
@@ -13,7 +13,10 @@ export declare const localMeshLinkKey: (record: EnrolledLocalMachine) => string;
13
13
  export declare function createLocalMeshLinks(options: {
14
14
  credentialPath: string;
15
15
  receive: (record: EnrolledLocalMachine, envelope: PostEnvelope) => Promise<PostDeliveryResult>;
16
- }): {
16
+ }, timing?: Readonly<{
17
+ wait: (ms: number, signal: AbortSignal) => Promise<void>;
18
+ random: () => number;
19
+ }>): {
17
20
  refresh: () => Promise<void>;
18
21
  isCurrent: (record: EnrolledLocalMachine) => Promise<boolean>;
19
22
  deliver: (record: EnrolledLocalMachine, envelope: PostEnvelope) => Promise<PostDeliveryResult>;
@@ -14,7 +14,10 @@ export const localMeshLinkKey = (record) => JSON.stringify([
14
14
  * Service-owned native I/O only. The account owns pins, Carrier owns retry
15
15
  * custody, and the addressed runtime owns acceptance. No mail is queued here.
16
16
  */
17
- export function createLocalMeshLinks(options) {
17
+ export function createLocalMeshLinks(options, timing = {
18
+ wait: (ms, signal) => delay(ms, undefined, { signal }),
19
+ random: Math.random,
20
+ }) {
18
21
  const account = createLocalMachineEnrollmentAccount(options.credentialPath);
19
22
  const links = new Map();
20
23
  let stopping = false;
@@ -33,6 +36,31 @@ export function createLocalMeshLinks(options) {
33
36
  !link.stop.signal.aborted &&
34
37
  (await current(link.record)));
35
38
  }
39
+ // WebSocket's error event does not expose the HTTP upgrade response. A
40
+ // freshly signed plain GET can surface the receiver's upgrade hint. The
41
+ // response is NOT signed: this is diagnostic only, never authority to stop.
42
+ async function upgradeAdvertised(record, signal) {
43
+ try {
44
+ const url = machineLinkDialUrl(record.authority.origin, record.scopeId, record.deviceId);
45
+ const signer = await importSeamSigner(`${record.deviceId}:${record.privateKey}`);
46
+ const signature = await signSeamRequest(signer, machineLinkAudience(record.scopeId, record.deviceId), "GET", url.href, unsignedPayload);
47
+ const response = await fetch(url, {
48
+ headers: { [seamHeader]: signature },
49
+ signal: AbortSignal.any([signal, AbortSignal.timeout(10_000)]),
50
+ });
51
+ await response.body?.cancel();
52
+ return (response.status === 426 &&
53
+ response.headers.get("x-fungi-machine-link-action") === "upgrade fungi");
54
+ }
55
+ catch {
56
+ return false;
57
+ }
58
+ }
59
+ /** Equal jitter avoids synchronized immediate retries even at the 15 s cap. */
60
+ const retryMs = (failures, random) => {
61
+ const ceiling = Math.min(15_000, 500 * 2 ** Math.min(failures, 5));
62
+ return Math.floor((ceiling * (1 + random)) / 2);
63
+ };
36
64
  async function connection(record, signal, publish) {
37
65
  const url = machineLinkDialUrl(record.authority.origin, record.scopeId, record.deviceId);
38
66
  const signer = await importSeamSigner(`${record.deviceId}:${record.privateKey}`);
@@ -161,21 +189,39 @@ export function createLocalMeshLinks(options) {
161
189
  stop,
162
190
  channel: undefined,
163
191
  done: (async () => {
192
+ let failures = 0;
193
+ let upgradeNotified = false;
164
194
  while (!stopped()) {
165
195
  try {
166
196
  if (!(await current(record)))
167
197
  return "stopped";
168
198
  await connection(record, stop.signal, (channel) => {
169
199
  link.channel = channel;
200
+ if (channel !== undefined) {
201
+ failures = 0;
202
+ upgradeNotified = false;
203
+ }
170
204
  });
171
205
  }
172
206
  catch (cause) {
173
207
  if (stopped())
174
208
  return "stopped";
175
209
  logEvent("mesh-link.connection", cause);
210
+ const advertised = await upgradeAdvertised(record, stop.signal);
211
+ if (stopped())
212
+ return "stopped";
213
+ if (advertised) {
214
+ // A forged refusal can delay a link, not permanently kill it.
215
+ // Emit once per outage and retry at the bounded cap.
216
+ if (!upgradeNotified)
217
+ console.error("upgrade fungi");
218
+ upgradeNotified = true;
219
+ failures = 5;
220
+ }
176
221
  }
177
222
  try {
178
- await delay(1000, undefined, { signal: stop.signal });
223
+ await timing.wait(retryMs(failures, timing.random()), stop.signal);
224
+ failures = Math.min(failures + 1, 5);
179
225
  }
180
226
  catch (cause) {
181
227
  if (stopped())
@@ -204,7 +250,8 @@ export function createLocalMeshLinks(options) {
204
250
  const link = links.get(localMeshLinkKey(record));
205
251
  const channel = link?.channel;
206
252
  if (channel === undefined || !(await isCurrent(record))) {
207
- link?.stop.abort();
253
+ // An unavailable send is Carrier's retry, not authority to retire a
254
+ // service-owned link while its redial is waiting.
208
255
  throw new MachineLinkPostUnavailable({ reason: "closed" });
209
256
  }
210
257
  return await channel.deliver(envelope);
@@ -1,2 +1,2 @@
1
1
  // Generated from native build output; not a runtime sidecar.
2
- export const nativeDigests = Object.freeze({"linux-x64":{"patch":"df8cca6e61abec1438f96b74cf415a1609caef3868ad2e0ebe0a8c20bd571b87","bwrap":"035ac5ddfc70dd7d4a9b575b351944d47aa9d0bbafeb433fb72b2afb02cef7f4","seccomp":"38112f9cf994bed2ed962fae63e803c41874732ab032088b2c1ea3d713e51c11"}});
2
+ export const nativeDigests = Object.freeze({"linux-x64":{"patch":"93d17ead57b33d2295cd3960603220b1c418084ab39bc8450a8fd90a258c7608","bwrap":"035ac5ddfc70dd7d4a9b575b351944d47aa9d0bbafeb433fb72b2afb02cef7f4","seccomp":"38112f9cf994bed2ed962fae63e803c41874732ab032088b2c1ea3d713e51c11","seccompWorkspace":"e46f1045f85fcf14f91fae8654bba6a0c87f57e3b92f60bfc16008dbf7dc87c8"}});
@@ -0,0 +1,2 @@
1
+ /** The Spore caller owns the release version; Local cannot infer it from its own build ID. */
2
+ export declare function nativePackageStartupError(cause?: unknown): Error;
@@ -0,0 +1,10 @@
1
+ import { z } from "zod";
2
+ /** The Spore caller owns the release version; Local cannot infer it from its own build ID. */
3
+ export function nativePackageStartupError(cause) {
4
+ const release = z.string().min(1).safeParse(process.env.FUNGI_SPORE_VERSION);
5
+ if (!release.success)
6
+ return new Error("Fungi CLI release version is unavailable; restart Fungi.", {
7
+ cause,
8
+ });
9
+ return new Error(`Fungi's native package for ${process.platform}-${process.arch} is missing or doesn't match this version. Reinstall: npm i -g @fungi.computer/spore@${release.data}`, { cause });
10
+ }
@@ -1,13 +1,12 @@
1
1
  import { type NativeSourceReader } from "./private-local-bwrap.js";
2
- import { type LocalSandboxPolicy, type NativeRunLivenessProbe } from "./private-local-sandbox-policy.js";
3
- /**
4
- * One invocation owner for pinned native bytes. Verify the installed source on
5
- * every admission, but execute/bind only the first private copy of those
6
- * verified bytes. The built digest pins each label to a single payload.
7
- */
8
- export declare function createNativePayloadStaging(project: string, readNativePatchSource?: NativeSourceReader, probeNativeRunLiveness?: NativeRunLivenessProbe): {
2
+ import { type LinuxSandboxProfile } from "./private-local-sandbox-policy.js";
3
+ /** The same metadata-independent package path selects the verified source and Files caller. */
4
+ export declare function packagedPatchPath(): string;
5
+ /** One verified private copy for the service's lifetime, not for each command. */
6
+ export declare function createNativePayloadStaging(project: string, readNativeSource?: NativeSourceReader): {
7
+ admitProject: (nextProject: string) => void;
9
8
  patch: (path: string) => string;
10
- linux: (policy: LocalSandboxPolicy) => {
9
+ linux: (profile: LinuxSandboxProfile) => {
11
10
  executable: string;
12
11
  seccompFilter: Uint8Array<ArrayBufferLike>;
13
12
  };
@@ -1,49 +1,138 @@
1
- import { rmSync } from "node:fs";
1
+ import { lstatSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync, } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import { userInfo } from "node:os";
4
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
+ import { fileURLToPath } from "node:url";
2
6
  import { localLinuxSandboxPayload, verifiedNativePatchHelper, LocalBwrapUnavailable, } from "./private-local-bwrap.js";
3
- import { privateNativeDirectory, stageNativeBytes, UnsafeNativePayload, } from "./private-local-sandbox-policy.js";
4
- /**
5
- * One invocation owner for pinned native bytes. Verify the installed source on
6
- * every admission, but execute/bind only the first private copy of those
7
- * verified bytes. The built digest pins each label to a single payload.
8
- */
9
- export function createNativePayloadStaging(project, readNativePatchSource, probeNativeRunLiveness) {
10
- let directory;
11
- let closed = false;
12
- const staged = new Map();
13
- const stage = (label, bytes) => {
14
- if (closed)
15
- throw new Error("Native staging owner is closed");
16
- const existing = staged.get(label);
17
- if (existing !== undefined)
18
- return existing;
7
+ import { assertHostOwnedNativeFile, UnsafeNativePayload, } from "./private-local-sandbox-policy.js";
8
+ function within(root, path) {
9
+ const tail = relative(root, path);
10
+ return !isAbsolute(tail) && tail !== ".." && !tail.startsWith(`..${sep}`);
11
+ }
12
+ /** The same metadata-independent package path selects the verified source and Files caller. */
13
+ export function packagedPatchPath() {
14
+ if (process.env.FUNGI_BUNDLED_CLI) {
15
+ const name = `@fungi.computer/spore-native-${process.platform}-${process.arch}`;
19
16
  try {
20
- directory ??= privateNativeDirectory(project, probeNativeRunLiveness);
21
- const path = stageNativeBytes(directory, label, bytes);
22
- staged.set(label, path);
23
- return path;
17
+ const paths = createRequire(process.env.FUNGI_BUNDLED_CLI).resolve.paths(name);
18
+ const packageRoot = paths
19
+ ?.map((directory) => join(directory, name))
20
+ .find((candidate) => lstatSync(candidate, { throwIfNoEntry: false }));
21
+ if (packageRoot !== undefined)
22
+ return join(packageRoot, "bin", "fungi-apply-patch");
23
+ throw new Error("Native package not installed");
24
24
  }
25
25
  catch (cause) {
26
- if (cause instanceof UnsafeNativePayload)
27
- throw cause;
28
- throw new LocalBwrapUnavailable("staging-failed", `Cannot stage verified ${label} in a private native directory; no command was started.`, { cause });
26
+ throw new LocalBwrapUnavailable("install", `Missing ${name}/bin/fungi-apply-patch; reinstall Fungi.`, { cause });
29
27
  }
30
- };
31
- return {
32
- patch: (path) => stage("fungi-apply-patch", verifiedNativePatchHelper(path, project, readNativePatchSource)),
33
- linux: (policy) => {
34
- const payload = localLinuxSandboxPayload(policy);
35
- return {
36
- executable: stage("fungi-bwrap", payload.executableBytes),
37
- seccompFilter: payload.seccompFilter,
38
- };
39
- },
40
- probeFilter: (bytes) => stage("fungi-seccomp.bpf", bytes),
41
- close: () => {
42
- if (closed)
43
- return;
44
- closed = true;
45
- if (directory !== undefined)
46
- rmSync(directory, { recursive: true });
47
- },
48
- };
28
+ }
29
+ return fileURLToPath(new URL("../native/bin/fungi-apply-patch", import.meta.url));
30
+ }
31
+ /** One verified private copy for the service's lifetime, not for each command. */
32
+ export function createNativePayloadStaging(project, readNativeSource = readFileSync) {
33
+ const patchSource = packagedPatchPath();
34
+ assertHostOwnedNativeFile(project, dirname(dirname(patchSource)));
35
+ const patchBytes = verifiedNativePatchHelper(patchSource, project, readNativeSource);
36
+ const linux = process.platform === "linux"
37
+ ? localLinuxSandboxPayload(project, readNativeSource)
38
+ : undefined;
39
+ const installDirectories = [
40
+ dirname(patchSource),
41
+ ...(linux === undefined ? [] : [dirname(linux.sourcePath)]),
42
+ ].flatMap((source) => [resolve(source), realpathSync(source)]);
43
+ const runtime = process.env.XDG_RUNTIME_DIR;
44
+ const base = runtime ?? join(userInfo().homedir, ".local", "state");
45
+ const uid = process.getuid?.();
46
+ if (uid === undefined || !isAbsolute(base))
47
+ throw new UnsafeNativePayload("Native staging needs an absolute runtime directory and POSIX user.");
48
+ if (runtime !== undefined) {
49
+ const metadata = lstatSync(runtime, { throwIfNoEntry: false });
50
+ if (metadata === undefined ||
51
+ !metadata.isDirectory() ||
52
+ metadata.uid !== uid ||
53
+ (metadata.mode & 0o077) !== 0)
54
+ throw new UnsafeNativePayload("XDG_RUNTIME_DIR must be a private directory owned by this user.");
55
+ }
56
+ const root = join(base, "fungi");
57
+ const projectRoot = realpathSync(project);
58
+ if (within(projectRoot, resolve(root)))
59
+ throw new UnsafeNativePayload("Host native staging cannot live inside the Agent project.");
60
+ let directory;
61
+ try {
62
+ mkdirSync(root, { recursive: true, mode: 0o700 });
63
+ const actual = realpathSync(root);
64
+ if (within(projectRoot, actual))
65
+ throw new UnsafeNativePayload("Host native staging resolves inside the Agent project.");
66
+ const metadata = lstatSync(actual);
67
+ if (!metadata.isDirectory() ||
68
+ metadata.uid !== uid ||
69
+ (metadata.mode & 0o077) !== 0)
70
+ throw new UnsafeNativePayload("Host native staging root must be private and owned by this user.");
71
+ const staged = mkdtempSync(join(actual, "native-"));
72
+ directory = staged;
73
+ const boundaries = [staged, ...installDirectories];
74
+ const stage = (label, bytes, mode) => {
75
+ const target = join(staged, label);
76
+ writeFileSync(target, bytes, { flag: "wx", mode });
77
+ return target;
78
+ };
79
+ const patch = stage("fungi-apply-patch", patchBytes, 0o500);
80
+ const bwrap = linux && stage("fungi-bwrap", linux.executableBytes, 0o500);
81
+ const seccomp = linux && stage("fungi-seccomp.bpf", linux.seccomp, 0o400);
82
+ const workspace = linux &&
83
+ stage("fungi-seccomp-workspace.bpf", linux.seccompWorkspace, 0o400);
84
+ let closed = false;
85
+ return {
86
+ admitProject: (nextProject) => {
87
+ if (closed)
88
+ throw new Error("Native staging owner is closed");
89
+ const next = realpathSync(nextProject);
90
+ for (const source of boundaries)
91
+ if (within(next, source))
92
+ throw new UnsafeNativePayload("Native files must be outside the Agent project.");
93
+ },
94
+ patch: (path) => {
95
+ if (closed)
96
+ throw new Error("Native staging owner is closed");
97
+ if (resolve(path) !== resolve(patchSource))
98
+ throw new LocalBwrapUnavailable("install", "Native patch helper changed; restart Fungi.");
99
+ return patch;
100
+ },
101
+ linux: (profile) => {
102
+ if (closed)
103
+ throw new Error("Native staging owner is closed");
104
+ if (linux === undefined || bwrap === undefined)
105
+ throw new LocalBwrapUnavailable("install", "No packaged Linux sandbox; reinstall Fungi.");
106
+ return {
107
+ executable: bwrap,
108
+ seccompFilter: profile.mode === "workspace"
109
+ ? linux.seccompWorkspace
110
+ : linux.seccomp,
111
+ };
112
+ },
113
+ probeFilter: (bytes) => {
114
+ if (closed)
115
+ throw new Error("Native staging owner is closed");
116
+ if (bytes === linux?.seccomp && seccomp !== undefined)
117
+ return seccomp;
118
+ if (bytes === linux?.seccompWorkspace && workspace !== undefined)
119
+ return workspace;
120
+ throw new LocalBwrapUnavailable("install", "Unknown native filter; restart Fungi.");
121
+ },
122
+ close: () => {
123
+ if (closed)
124
+ return;
125
+ closed = true;
126
+ rmSync(staged, { recursive: true });
127
+ },
128
+ };
129
+ }
130
+ catch (cause) {
131
+ if (directory !== undefined)
132
+ rmSync(directory, { recursive: true });
133
+ if (cause instanceof UnsafeNativePayload ||
134
+ cause instanceof LocalBwrapUnavailable)
135
+ throw cause;
136
+ throw new LocalBwrapUnavailable("install", "Cannot copy verified native files into the private runtime directory; reinstall Fungi.", { cause });
137
+ }
49
138
  }
@@ -0,0 +1,10 @@
1
+ import { z } from "zod";
2
+ export declare const localPermissionModeSchema: z.ZodEnum<{
3
+ "read-only": "read-only";
4
+ workspace: "workspace";
5
+ "full-access": "full-access";
6
+ }>;
7
+ export type LocalPermissionMode = z.output<typeof localPermissionModeSchema>;
8
+ export declare function modeWithinCeiling(candidate: LocalPermissionMode, creator: LocalPermissionMode): boolean;
9
+ /** Levi's 2026-10-25 Q1 decision: default Workspace when the host sandbox probe passes, else Full access. */
10
+ export declare function defaultLocalPermissionMode(sandboxAvailable: boolean): LocalPermissionMode;
@@ -0,0 +1,18 @@
1
+ import { z } from "zod";
2
+ export const localPermissionModeSchema = z.enum([
3
+ "read-only",
4
+ "workspace",
5
+ "full-access",
6
+ ]);
7
+ const rank = {
8
+ "read-only": 0,
9
+ workspace: 1,
10
+ "full-access": 2,
11
+ };
12
+ export function modeWithinCeiling(candidate, creator) {
13
+ return rank[candidate] <= rank[creator];
14
+ }
15
+ /** Levi's 2026-10-25 Q1 decision: default Workspace when the host sandbox probe passes, else Full access. */
16
+ export function defaultLocalPermissionMode(sandboxAvailable) {
17
+ return sandboxAvailable ? "workspace" : "full-access";
18
+ }
@@ -2,6 +2,7 @@ import type { OutstandingDelegation } from "@fungi.computer/shiitake/fleet";
2
2
  import { createMcpFormSession } from "./private-mcp-form-session.js";
3
3
  import type { LocalUiAttachment } from "./private-local-ui-attachment.js";
4
4
  import { type LocalAgentExtensions } from "./local-extensions.js";
5
+ import { type LocalPermissionMode } from "./private-local-permission-mode.js";
5
6
  import { type Models, type ThinkingLevel } from "@earendil-works/pi-ai";
6
7
  import type { CapabilitySource } from "@fungi.computer/mycelium";
7
8
  import type { BirdDogMail } from "@fungi.computer/bird-dog";
@@ -12,8 +13,10 @@ import type { ExtensionRosterV0 } from "@fungi.computer/shiitake/extensions";
12
13
  import { type InstructionSource, type ModelSource, Shiitake, type RuntimeHost, type ShiitakeAgent, type ShiitakeSession, type ShiitakeOptions } from "@fungi.computer/shiitake";
13
14
  import { type PrivateLocalConfigRoot } from "./private-local-config-root.js";
14
15
  import { type PrivateLocalEffectiveModel } from "./private-local-effective-model.js";
16
+ import { type LocalExecEscapeStage, type LocalExecPolicyRun } from "./private-invocation-computer.js";
15
17
  import { type PrivateLocalComputerProcesses } from "./private-local-computer-processes.js";
16
18
  import { type LocalSandboxControls, type LocalSandboxStatus } from "./private-local-sandbox.js";
19
+ import type { NativePayloadStaging } from "./private-local-native-staging.js";
17
20
  import { type NodeRuntimeHost, type NodeStoreLease } from "@fungi.computer/shiitake/node";
18
21
  import { createPrivateInvocationFilesPort, type LocalSandboxWrites } from "./private-invocation-files.js";
19
22
  import { type PrivateFungiMcpConnectionOptions } from "./private-fungi-mcp-connection.js";
@@ -49,9 +52,17 @@ export type PrivateLocalTools = Readonly<{
49
52
  export type PrivateLocalRuntimeHostOptions = Readonly<{
50
53
  /** Service-selected host configuration, independent of this Agent's Files root. */
51
54
  configRoot?: PrivateLocalConfigRoot;
55
+ /** Verified service copy; the service, not the Agent, closes it. */
56
+ native?: NativePayloadStaging | undefined;
52
57
  configuration?: (storeDirectory: string) => Promise<LocalRuntimeConfiguration>;
53
58
  /** Invocation root captured before selecting a fresh local database. */
54
59
  invocationRoot: string;
60
+ /** Service-owned nonsecret audit sink, never a command or environment logger. */
61
+ onExecRuleMatch?: (match: Readonly<{
62
+ agentId: string;
63
+ executable: string;
64
+ ruleId: string;
65
+ }>) => void;
55
66
  /** Existing Agent database to reopen instead of creating a fresh local store. */
56
67
  databasePath?: string | undefined;
57
68
  /** Stable Local store identity for replaying one Agent create request. */
@@ -63,7 +74,7 @@ export type PrivateLocalRuntimeHostOptions = Readonly<{
63
74
  /** Immutable project-relative folder authority, inherited by child Agents. */
64
75
  folder?: string | undefined;
65
76
  /** Persist before constructing the Agent, which can recover durable work. */
66
- requireSandbox?: boolean | undefined;
77
+ mode?: LocalPermissionMode | undefined;
67
78
  /** Explicit Agent model; otherwise the Agent's stored default, then the available login. */
68
79
  providerId?: string | undefined;
69
80
  modelId?: string | undefined;
@@ -107,8 +118,8 @@ export type PrivateLocalRuntimeHost = Readonly<{
107
118
  * flag changes; the store, its Sessions and files stay.
108
119
  */
109
120
  setArchived: (archived: boolean) => void;
110
- /** Persist the requirement before another affected launch is admitted. */
111
- setRequireSandbox: (required: boolean) => void;
121
+ /** Persist a person's mode before another affected launch is admitted. */
122
+ setMode: (mode: LocalPermissionMode) => void;
112
123
  sandboxStatus: () => LocalSandboxStatus;
113
124
  /** Set the persistent Agent default, thinking level included, without changing Session overrides. */
114
125
  setModel: (model: ModelPreference, signal: AbortSignal) => Promise<void>;
@@ -135,7 +146,10 @@ export type PrivateLocalRuntimeHost = Readonly<{
135
146
  * owed a delivery attempt or running background process. Work it delegated
136
147
  * is owed by the child, whose reply mail reopens this Agent.
137
148
  */
138
- idle: () => Promise<boolean>;
149
+ idle: () => Promise<{
150
+ idle: boolean;
151
+ wakeAt: number | null;
152
+ }>;
139
153
  close: () => Promise<void>;
140
154
  }>;
141
155
  export type LocalRuntimeConfiguration = Readonly<{
@@ -171,7 +185,7 @@ storeDirectory?: string,
171
185
  subagents?: Readonly<{
172
186
  control: CapabilitySource;
173
187
  source: SubagentSource;
174
- }>, configuration?: LocalRuntimeConfiguration, configRoot?: PrivateLocalConfigRoot, sandbox?: LocalSandboxWrites): Promise<PrivateLocalTools>;
188
+ }>, configuration?: LocalRuntimeConfiguration, configRoot?: PrivateLocalConfigRoot, sandbox?: LocalSandboxWrites, currentMode?: () => LocalPermissionMode, escapeForSession?: (sessionId: string) => LocalExecEscapeStage, policyForSession?: (sessionId: string) => LocalExecPolicyRun): Promise<PrivateLocalTools>;
175
189
  /** Event-loop adaptation for the one local Shiitake owner; it has no queue policy. */
176
190
  export declare function createPrivateLocalRuntimeHost(): RuntimeHost;
177
191
  /**