@fungi.computer/truffle 0.2.1 → 0.2.2

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.
@@ -6,6 +6,7 @@ import * as ChildProcess from "effect/process/ChildProcess";
6
6
  import { parsePatch } from "diff";
7
7
  import * as Stream from "effect/Stream";
8
8
  import { runPrivateInvocationProcess, } from "./private-invocation-computer.js";
9
+ import { createNativePayloadStaging } from "./private-local-native-staging.js";
9
10
  function patchExecutable() {
10
11
  if (process.env.FUNGI_BUNDLED_CLI) {
11
12
  const name = `@fungi.computer/spore-native-${process.platform}-${process.arch}`;
@@ -51,7 +52,9 @@ async function unifiedDiffContext(patch, read, context) {
51
52
  export async function createPrivateFilesPatch(root, read, sandbox) {
52
53
  const executable = patchExecutable();
53
54
  try {
54
- await access(executable, constants.X_OK);
55
+ // Advertise the installed helper if present; only the verified private
56
+ // copy is executed, so its source does not need an execute bit.
57
+ await access(executable, constants.F_OK);
55
58
  }
56
59
  catch (cause) {
57
60
  if (cause instanceof Error && "code" in cause && cause.code === "ENOENT")
@@ -63,27 +66,41 @@ export async function createPrivateFilesPatch(root, read, sandbox) {
63
66
  const bytes = new TextEncoder().encode(patch);
64
67
  if (bytes.byteLength > maximumPatchBytes)
65
68
  throw new Error("Patch exceeds the 128 KiB input bound");
66
- const launch = sandbox?.(root, [executable]);
67
- const env = { ...process.env, ...launch?.environment };
68
- if (launch !== undefined) {
69
- delete env.XDG_RUNTIME_DIR;
70
- delete env.DBUS_SESSION_BUS_ADDRESS;
71
- }
72
- const command = ChildProcess.make(launch?.executable ?? executable, launch?.args ?? [], {
73
- cwd: root,
74
- env,
75
- extendEnv: false,
76
- stdin: Stream.make(bytes),
77
- additionalFds: launch?.seccompFilter === undefined
78
- ? undefined
79
- : {
80
- fd3: {
81
- type: "input",
82
- stream: Stream.make(launch.seccompFilter),
69
+ let result;
70
+ let standalone;
71
+ try {
72
+ const launch = sandbox?.(root, [executable], executable);
73
+ // Without controls, this one Files invocation owns and closes its copy.
74
+ if (launch === undefined)
75
+ standalone = createNativePayloadStaging(root);
76
+ const privateExecutable = standalone?.patch(executable);
77
+ const executableToRun = launch?.executable ?? privateExecutable;
78
+ if (executableToRun === undefined)
79
+ throw new Error("Native Files helper was not staged");
80
+ const env = { ...process.env, ...launch?.environment };
81
+ if (launch !== undefined && !("bareNative" in launch)) {
82
+ delete env.XDG_RUNTIME_DIR;
83
+ delete env.DBUS_SESSION_BUS_ADDRESS;
84
+ }
85
+ const command = ChildProcess.make(executableToRun, launch?.args ?? [], {
86
+ cwd: root,
87
+ env,
88
+ extendEnv: false,
89
+ stdin: Stream.make(bytes),
90
+ additionalFds: launch?.seccompFilter === undefined
91
+ ? undefined
92
+ : {
93
+ fd3: {
94
+ type: "input",
95
+ stream: Stream.make(launch.seccompFilter),
96
+ },
83
97
  },
84
- },
85
- });
86
- const result = await runPrivateInvocationProcess(command, context.signal, 30_000);
98
+ });
99
+ result = await runPrivateInvocationProcess(command, context.signal, 30_000);
100
+ }
101
+ finally {
102
+ standalone?.close();
103
+ }
87
104
  if (result.status === "deadline_exceeded")
88
105
  return {
89
106
  status: "failed",
@@ -3,8 +3,15 @@ import type { KnapsackComputer } from "@fungi.computer/knapsack";
3
3
  import { z } from "zod";
4
4
  import { type PrivateLocalProcessScope } from "./private-local-computer-processes.js";
5
5
  import type { SandboxCommand } from "./private-local-containment.js";
6
- /** The host admits each launch; undefined is its explicit unavailable fallback. */
7
- export type LocalSandboxLaunch = (cwd: string, argv: readonly [string, ...string[]]) => SandboxCommand | undefined;
6
+ /** A bare fallback for the native Files engine still names only staged bytes. */
7
+ export type BareNativeCommand = SandboxCommand & Readonly<{
8
+ bareNative: true;
9
+ }>;
10
+ /** Only Files supplies a helper; generic exec can still fall back to its argv. */
11
+ export type LocalSandboxLaunch = {
12
+ (cwd: string, argv: readonly [string, ...string[]]): SandboxCommand | undefined;
13
+ (cwd: string, argv: readonly [string, ...string[]], readOnlyHelperFile: string): SandboxCommand | BareNativeCommand;
14
+ };
8
15
  declare const execOutput: z.ZodDiscriminatedUnion<[z.ZodObject<{
9
16
  exitCode: z.ZodNumber;
10
17
  stdout: z.ZodString;
@@ -1,13 +1,21 @@
1
+ import { type LocalSandboxPolicy } from "./private-local-sandbox-policy.js";
1
2
  export declare class LocalBwrapUnavailable extends Error {
2
- readonly reason: "missing" | "invalid-digest" | "digest-mismatch" | "not-executable";
3
+ readonly reason: "missing" | "missing-artifact" | "invalid-digest" | "digest-mismatch" | "unreadable" | "not-executable" | "staging-failed";
3
4
  readonly name = "LocalBwrapUnavailable";
4
- constructor(reason: "missing" | "invalid-digest" | "digest-mismatch" | "not-executable", message: string, options?: ErrorOptions);
5
+ constructor(reason: "missing" | "missing-artifact" | "invalid-digest" | "digest-mismatch" | "unreadable" | "not-executable" | "staging-failed", message: string, options?: ErrorOptions);
5
6
  }
6
7
  export type LinuxSandboxPayload = {
7
- readonly executable: string;
8
+ readonly executableBytes: Uint8Array;
8
9
  readonly seccompFilter: Uint8Array;
9
10
  };
11
+ export type LinuxSandboxDigests = Readonly<{
12
+ bwrap: string;
13
+ seccomp: string;
14
+ }>;
15
+ /** Packaged patch has no sidecar; the built module owns its digest. */
16
+ export type NativeSourceReader = (path: string) => Buffer;
17
+ export declare function verifiedNativePatchHelper(path: string, project: string, readNativeSource?: NativeSourceReader): Uint8Array;
10
18
  /** Check both packaged artifacts on every use; never cache verification. */
11
- export declare function verifyLocalLinuxSandboxPayload(executable: string): LinuxSandboxPayload;
19
+ export declare function verifyLocalLinuxSandboxPayload(executable: string, policy: LocalSandboxPolicy, expected: LinuxSandboxDigests): LinuxSandboxPayload;
12
20
  /** Same optional native-package lookup as apply_patch, with no PATH fallback. */
13
- export declare function localLinuxSandboxPayload(): LinuxSandboxPayload;
21
+ export declare function localLinuxSandboxPayload(policy: LocalSandboxPolicy): LinuxSandboxPayload;
@@ -1,13 +1,10 @@
1
1
  import { createHash } from "node:crypto";
2
- import { accessSync, constants, readFileSync, statSync } from "node:fs";
2
+ import { lstatSync, readFileSync, statSync } from "node:fs";
3
3
  import { createRequire } from "node:module";
4
4
  import { dirname, join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
- import { z } from "zod";
7
- const digestSchema = z
8
- .string()
9
- .trim()
10
- .regex(/^[a-f0-9]{64}$/u);
6
+ import { nativeDigests } from "./private-local-native-digests.js";
7
+ import { assertHostOwnedNativeFile, UnsafeNativePayload, } from "./private-local-sandbox-policy.js";
11
8
  export class LocalBwrapUnavailable extends Error {
12
9
  reason;
13
10
  name = "LocalBwrapUnavailable";
@@ -16,55 +13,93 @@ export class LocalBwrapUnavailable extends Error {
16
13
  this.reason = reason;
17
14
  }
18
15
  }
16
+ export function verifiedNativePatchHelper(path, project, readNativeSource = readFileSync) {
17
+ const expected = nativeDigests[`${process.platform}-${process.arch}`]?.patch;
18
+ if (expected === undefined)
19
+ throw new LocalBwrapUnavailable("invalid-digest", "No built native patch digest for this platform; reinstall Fungi.");
20
+ return verifiedBytes(path, "fungi-apply-patch", project, expected, readNativeSource);
21
+ }
19
22
  /** Check both packaged artifacts on every use; never cache verification. */
20
- export function verifyLocalLinuxSandboxPayload(executable) {
21
- verifiedBytes(executable, "fungi-bwrap");
22
- try {
23
- accessSync(executable, constants.X_OK);
24
- }
25
- catch (cause) {
26
- throw new LocalBwrapUnavailable("not-executable", "Packaged fungi-bwrap cannot be executed; reinstall Fungi.", { cause });
27
- }
23
+ export function verifyLocalLinuxSandboxPayload(executable, policy, expected) {
24
+ const executableBytes = verifiedBytes(executable, "fungi-bwrap", policy.projectRoot, expected.bwrap);
28
25
  return {
29
- executable,
30
- seccompFilter: verifiedBytes(join(dirname(executable), "fungi-seccomp.bpf"), "fungi-seccomp.bpf"),
26
+ executableBytes,
27
+ seccompFilter: verifiedBytes(join(dirname(executable), "fungi-seccomp.bpf"), "fungi-seccomp.bpf", policy.projectRoot, expected.seccomp),
31
28
  };
32
29
  }
33
- function verifiedBytes(path, label) {
30
+ function verifiedBytes(path, label, project, expected, readNativeSource = readFileSync) {
34
31
  let bytes;
35
- let digest;
36
32
  try {
33
+ assertHostOwnedNativeFile(project, path);
37
34
  if (!statSync(path).isFile())
38
35
  throw new LocalBwrapUnavailable("not-executable", `Packaged ${label} is not a regular file; reinstall Fungi.`);
39
- bytes = readFileSync(path);
40
- digest = readFileSync(`${path}.sha256`, "utf8");
36
+ bytes = readNativeSource(path);
41
37
  }
42
38
  catch (cause) {
43
39
  if (cause instanceof Error && "code" in cause && cause.code === "ENOENT")
44
- throw new LocalBwrapUnavailable("missing", `Packaged ${label} or its digest is missing; reinstall Fungi with optional dependencies enabled.`, { cause });
45
- throw cause;
40
+ throw new LocalBwrapUnavailable("missing-artifact", `Packaged ${label} is missing; reinstall Fungi with optional dependencies enabled.`, { cause });
41
+ if (cause instanceof UnsafeNativePayload ||
42
+ cause instanceof LocalBwrapUnavailable)
43
+ throw cause;
44
+ throw new LocalBwrapUnavailable("unreadable", `Packaged ${label} cannot be read; reinstall Fungi. No command was started.`, { cause });
46
45
  }
47
- const expected = digestSchema.safeParse(digest);
48
- if (!expected.success)
49
- throw new LocalBwrapUnavailable("invalid-digest", `Packaged ${label} has an invalid SHA-256 record; reinstall Fungi.`);
50
- if (createHash("sha256").update(bytes).digest("hex") !== expected.data)
46
+ if (createHash("sha256").update(bytes).digest("hex") !== expected)
51
47
  throw new LocalBwrapUnavailable("digest-mismatch", `Packaged ${label} digest mismatch; reinstall Fungi. No command was started.`);
52
48
  return bytes;
53
49
  }
54
50
  /** Same optional native-package lookup as apply_patch, with no PATH fallback. */
55
- export function localLinuxSandboxPayload() {
51
+ export function localLinuxSandboxPayload(policy) {
52
+ const digest = nativeDigests[`${process.platform}-${process.arch}`];
53
+ if (digest?.bwrap === undefined || digest.seccomp === undefined)
54
+ throw new LocalBwrapUnavailable("invalid-digest", "No built native Linux payload digest for this platform; reinstall Fungi.");
56
55
  let executable;
57
56
  if (process.env.FUNGI_BUNDLED_CLI) {
58
57
  const name = `@fungi.computer/spore-native-${process.platform}-${process.arch}`;
58
+ const message = `Packaged ${name}/bin/fungi-bwrap cannot be read; reinstall Fungi.`;
59
+ let directories;
60
+ try {
61
+ directories = createRequire(process.env.FUNGI_BUNDLED_CLI).resolve.paths(name);
62
+ }
63
+ catch (cause) {
64
+ throw new LocalBwrapUnavailable("missing-artifact", message, { cause });
65
+ }
66
+ if (directories === null)
67
+ throw new LocalBwrapUnavailable("missing-artifact", message);
68
+ let packageRoot;
69
+ for (const directory of directories) {
70
+ const candidate = join(directory, name);
71
+ try {
72
+ if (lstatSync(candidate, { throwIfNoEntry: false }) !== undefined) {
73
+ packageRoot = candidate;
74
+ break;
75
+ }
76
+ }
77
+ catch (cause) {
78
+ throw new LocalBwrapUnavailable("missing-artifact", message, { cause });
79
+ }
80
+ }
81
+ if (packageRoot === undefined)
82
+ throw new LocalBwrapUnavailable("missing", `Missing ${name}; reinstall Fungi with optional dependencies enabled.`);
83
+ // Package metadata (including exports) is not an authority for whether
84
+ // native bytes are absent. Check the package's location before its bin.
59
85
  try {
60
- executable = createRequire(process.env.FUNGI_BUNDLED_CLI).resolve(`${name}/bin/fungi-bwrap`);
86
+ assertHostOwnedNativeFile(policy.projectRoot, packageRoot);
87
+ if (!statSync(packageRoot).isDirectory())
88
+ throw new LocalBwrapUnavailable("missing-artifact", message);
61
89
  }
62
90
  catch (cause) {
63
- throw new LocalBwrapUnavailable("missing", `Missing ${name}/bin/fungi-bwrap; reinstall Fungi with optional dependencies enabled.`, { cause });
91
+ if (cause instanceof UnsafeNativePayload ||
92
+ cause instanceof LocalBwrapUnavailable)
93
+ throw cause;
94
+ throw new LocalBwrapUnavailable("missing-artifact", message, { cause });
64
95
  }
96
+ executable = join(packageRoot, "bin", "fungi-bwrap");
65
97
  }
66
98
  else {
67
99
  executable = fileURLToPath(new URL("../native/bin/fungi-bwrap", import.meta.url));
68
100
  }
69
- return verifyLocalLinuxSandboxPayload(executable);
101
+ return verifyLocalLinuxSandboxPayload(executable, policy, {
102
+ bwrap: digest.bwrap,
103
+ seccomp: digest.seccomp,
104
+ });
70
105
  }
@@ -1,12 +1,19 @@
1
+ import { type NativePayloadStaging } from "./private-local-native-staging.js";
1
2
  import { z } from "zod";
2
3
  export declare const sandboxUnavailableReasonSchema: z.ZodObject<{
3
4
  kind: z.ZodEnum<{
4
5
  missing: "missing";
6
+ "missing-artifact": "missing-artifact";
5
7
  "invalid-digest": "invalid-digest";
6
8
  "digest-mismatch": "digest-mismatch";
9
+ unreadable: "unreadable";
7
10
  "not-executable": "not-executable";
11
+ "staging-failed": "staging-failed";
8
12
  "unsupported-platform": "unsupported-platform";
9
13
  "invalid-folder": "invalid-folder";
14
+ "unsafe-native-payload": "unsafe-native-payload";
15
+ "unsafe-host-code": "unsafe-host-code";
16
+ "startup-refused": "startup-refused";
10
17
  "backend-refused": "backend-refused";
11
18
  }>;
12
19
  message: z.ZodString;
@@ -28,7 +35,7 @@ export type SandboxCommand = Readonly<{
28
35
  }>;
29
36
  /** Existing foreground/background owners retain process and cancellation lifetime. */
30
37
  export type LocalSandbox = Readonly<{
31
- command(cwd: string, argv: readonly [string, ...string[]]): SandboxCommand;
38
+ command(cwd: string, argv: readonly [string, ...string[]], readOnlyHelperFile?: string): SandboxCommand;
32
39
  close(): void;
33
40
  }>;
34
41
  export type SandboxAdmission = Readonly<{
@@ -38,6 +45,7 @@ export type SandboxAdmission = Readonly<{
38
45
  state: "unavailable";
39
46
  reason: SandboxUnavailableReason;
40
47
  }>;
48
+ export declare function sandboxUnavailableReason(cause: unknown): SandboxUnavailableReason;
41
49
  /**
42
50
  * The caller already owns the person's auto-approve choice. This module has no
43
51
  * mode, setting, bare-command branch, PATH fallback or permission to widen it.
@@ -48,4 +56,4 @@ export declare function establishLocalSandbox(input: Readonly<{
48
56
  /** Absolute folder selected by the Local owner. */
49
57
  folder: string;
50
58
  platform?: string;
51
- }>): SandboxAdmission;
59
+ }>, staging?: NativePayloadStaging): SandboxAdmission;
@@ -1,20 +1,27 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import { closeSync, mkdtempSync, openSync, realpathSync, rmSync, writeFileSync, } from "node:fs";
2
+ import { closeSync, mkdtempSync, openSync, realpathSync, rmSync, } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
- import { LocalBwrapUnavailable, localLinuxSandboxPayload, } from "./private-local-bwrap.js";
5
+ import { LocalBwrapUnavailable } from "./private-local-bwrap.js";
6
+ import { createNativePayloadStaging, } from "./private-local-native-staging.js";
6
7
  import { linuxSandboxCommand } from "./private-local-linux-sandbox.js";
7
8
  import { seatbeltCommand } from "./private-local-seatbelt.js";
8
- import { sandboxPolicy } from "./private-local-sandbox-policy.js";
9
+ import { sandboxPolicy, UnsafeHostCode, UnsafeNativePayload, } from "./private-local-sandbox-policy.js";
9
10
  import { z } from "zod";
10
11
  export const sandboxUnavailableReasonSchema = z.strictObject({
11
12
  kind: z.enum([
12
13
  "unsupported-platform",
13
14
  "invalid-folder",
14
15
  "missing",
16
+ "missing-artifact",
15
17
  "invalid-digest",
16
18
  "digest-mismatch",
19
+ "unreadable",
17
20
  "not-executable",
21
+ "staging-failed",
22
+ "unsafe-native-payload",
23
+ "unsafe-host-code",
24
+ "startup-refused",
18
25
  "backend-refused",
19
26
  ]),
20
27
  message: z.string(),
@@ -27,7 +34,11 @@ export class LocalSandboxUnavailable extends Error {
27
34
  this.reason = reason;
28
35
  }
29
36
  }
30
- function unavailable(cause) {
37
+ export function sandboxUnavailableReason(cause) {
38
+ if (cause instanceof UnsafeHostCode)
39
+ return { kind: "unsafe-host-code", message: cause.message };
40
+ if (cause instanceof UnsafeNativePayload)
41
+ return { kind: "unsafe-native-payload", message: cause.message };
31
42
  if (cause instanceof LocalBwrapUnavailable)
32
43
  return { kind: cause.reason, message: cause.message };
33
44
  return {
@@ -40,7 +51,7 @@ function unavailable(cause) {
40
51
  * mode, setting, bare-command branch, PATH fallback or permission to widen it.
41
52
  * A ready result proves only that the backend can start, not the escape matrix.
42
53
  */
43
- export function establishLocalSandbox(input) {
54
+ export function establishLocalSandbox(input, staging) {
44
55
  try {
45
56
  sandboxPolicy(input.project, input.folder);
46
57
  }
@@ -62,14 +73,17 @@ export function establishLocalSandbox(input) {
62
73
  message: `Local sandbox is unavailable on ${platform}.`,
63
74
  },
64
75
  };
65
- let temporaryDirectory;
76
+ const native = staging ?? createNativePayloadStaging(input.project);
77
+ let agentTemporaryDirectory;
66
78
  try {
67
- // A sandbox owns only its own temporary directory, never the host's HOME.
68
- temporaryDirectory = realpathSync(mkdtempSync(join(tmpdir(), "fungi-sandbox-")));
69
- const temporary = temporaryDirectory;
79
+ // Native staging is never granted as an Agent-writable temporary root.
80
+ if (platform === "darwin")
81
+ agentTemporaryDirectory = realpathSync(mkdtempSync(join(tmpdir(), "fungi-sandbox-")));
82
+ const agentTemporary = agentTemporaryDirectory;
83
+ const environmentTmp = agentTemporary ?? "/tmp";
70
84
  let closed = false;
71
85
  const sandbox = {
72
- command: (cwd, argv) => {
86
+ command: (cwd, argv, readOnlyHelperFile) => {
73
87
  if (closed)
74
88
  throw new LocalSandboxUnavailable({
75
89
  kind: "backend-refused",
@@ -79,23 +93,45 @@ export function establishLocalSandbox(input) {
79
93
  // it must not turn into an authorized unsandboxed retry.
80
94
  const policy = sandboxPolicy(input.project, input.folder);
81
95
  try {
82
- const launch = platform === "linux"
83
- ? linuxSandboxCommand(policy, localLinuxSandboxPayload(), cwd, argv)
84
- : seatbeltCommand({ ...policy, temporaryDirectory: temporary }, argv);
96
+ const linuxPayload = platform === "linux" ? native.linux(policy) : undefined;
97
+ const stagedHelper = readOnlyHelperFile === undefined
98
+ ? undefined
99
+ : native.patch(readOnlyHelperFile);
100
+ const stagedArgv = stagedHelper === undefined
101
+ ? argv
102
+ : [
103
+ argv[0] === readOnlyHelperFile ? stagedHelper : argv[0],
104
+ ...argv
105
+ .slice(1)
106
+ .map((argument) => argument === readOnlyHelperFile ? stagedHelper : argument),
107
+ ];
108
+ const launch = linuxPayload !== undefined
109
+ ? linuxSandboxCommand(policy, linuxPayload, cwd, stagedArgv, undefined, stagedHelper)
110
+ : seatbeltCommand({
111
+ ...policy,
112
+ temporaryDirectory: environmentTmp,
113
+ }, stagedArgv);
85
114
  return {
86
115
  ...launch,
87
- environment: { TMPDIR: platform === "linux" ? "/tmp" : temporary },
116
+ environment: { TMPDIR: environmentTmp },
88
117
  };
89
118
  }
90
119
  catch (cause) {
91
- throw new LocalSandboxUnavailable(unavailable(cause));
120
+ throw new LocalSandboxUnavailable(sandboxUnavailableReason(cause));
92
121
  }
93
122
  },
94
123
  close: () => {
95
124
  if (closed)
96
125
  return;
97
126
  closed = true;
98
- rmSync(temporary, { recursive: true });
127
+ try {
128
+ if (staging === undefined)
129
+ native.close();
130
+ }
131
+ finally {
132
+ if (agentTemporary !== undefined)
133
+ rmSync(agentTemporary, { recursive: true });
134
+ }
99
135
  },
100
136
  };
101
137
  const command = sandbox.command(realpathSync(input.folder), [
@@ -104,8 +140,7 @@ export function establishLocalSandbox(input) {
104
140
  let filterDescriptor;
105
141
  const probe = () => {
106
142
  if (command.seccompFilter !== undefined) {
107
- const path = join(temporary, "seccomp.bpf");
108
- writeFileSync(path, command.seccompFilter, { mode: 0o600 });
143
+ const path = native.probeFilter(command.seccompFilter);
109
144
  filterDescriptor = openSync(path, "r");
110
145
  }
111
146
  return spawnSync(command.executable, [...command.args], {
@@ -134,11 +169,11 @@ export function establishLocalSandbox(input) {
134
169
  })();
135
170
  if (result.status !== 0) {
136
171
  sandbox.close();
137
- const stderr = result.stderr.trim();
172
+ const stderr = result.error === undefined ? result.stderr.trim() : "";
138
173
  return {
139
174
  state: "unavailable",
140
175
  reason: {
141
- kind: "backend-refused",
176
+ kind: "startup-refused",
142
177
  message: result.error?.message ??
143
178
  (stderr.length > 0
144
179
  ? stderr
@@ -149,13 +184,19 @@ export function establishLocalSandbox(input) {
149
184
  return { state: "ready", sandbox };
150
185
  }
151
186
  catch (cause) {
152
- if (temporaryDirectory !== undefined)
153
- rmSync(temporaryDirectory, { recursive: true });
187
+ try {
188
+ if (staging === undefined)
189
+ native.close();
190
+ }
191
+ finally {
192
+ if (agentTemporaryDirectory !== undefined)
193
+ rmSync(agentTemporaryDirectory, { recursive: true });
194
+ }
154
195
  return {
155
196
  state: "unavailable",
156
197
  reason: cause instanceof LocalSandboxUnavailable
157
198
  ? cause.reason
158
- : unavailable(cause),
199
+ : sandboxUnavailableReason(cause),
159
200
  };
160
201
  }
161
202
  }
@@ -1,8 +1,10 @@
1
1
  import { type LocalSandboxPolicy } from "./private-local-sandbox-policy.js";
2
2
  import type { LinuxSandboxPayload } from "./private-local-bwrap.js";
3
3
  /** Ordinary upstream bwrap argv. There is no approval or bare-command path. */
4
- export declare function linuxSandboxCommand(policy: LocalSandboxPolicy, payload: LinuxSandboxPayload, cwd: string, argv: readonly [string, ...string[]], host?: NodeJS.ProcessEnv): {
5
- args: string[];
4
+ export declare function linuxSandboxCommand(policy: LocalSandboxPolicy, payload: Omit<LinuxSandboxPayload, "executableBytes"> & {
6
5
  executable: string;
6
+ }, cwd: string, argv: readonly [string, ...string[]], host?: NodeJS.ProcessEnv, readOnlyHelperFile?: string): {
7
+ args: string[];
7
8
  seccompFilter: Uint8Array;
9
+ executable: string;
8
10
  };
@@ -2,15 +2,21 @@ import { existsSync, mkdirSync, realpathSync, statSync, writeFileSync, } from "n
2
2
  import { isAbsolute, join } 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
- export function linuxSandboxCommand(policy, payload, cwd, argv, host = process.env) {
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);
9
10
  for (const git of policy.immovable) {
10
- mkdirSync(join(git, "hooks"), { recursive: true });
11
+ if (reserved.has(join(git, "hooks")))
12
+ mkdirSync(join(git, "hooks"), { recursive: true });
11
13
  for (const config of ["config", "config.worktree"])
12
- if (!existsSync(join(git, config)))
14
+ if (reserved.has(join(git, config)) && !existsSync(join(git, config)))
13
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" });
14
20
  }
15
21
  const args = [
16
22
  "--unshare-all",
@@ -62,6 +68,13 @@ export function linuxSandboxCommand(policy, payload, cwd, argv, host = process.e
62
68
  existsSync(agentSocket) &&
63
69
  statSync(agentSocket).isSocket())
64
70
  args.push("--ro-bind", "/dev/null", realpathSync(agentSocket));
71
+ // Native Files passes its resolved helper, never an untrusted exec argv[0].
72
+ // Re-expose only that file after private mounts; its siblings stay hidden.
73
+ if (readOnlyHelperFile !== undefined) {
74
+ if (!statSync(readOnlyHelperFile).isFile())
75
+ throw new Error("Local native helper must be a regular file");
76
+ args.push("--ro-bind", readOnlyHelperFile, readOnlyHelperFile);
77
+ }
65
78
  args.push("--chdir", cwd, "--", ...argv);
66
79
  return { ...payload, args };
67
80
  }
@@ -0,0 +1,2 @@
1
+ // Generated from native build output; not a runtime sidecar.
2
+ export const nativeDigests = Object.freeze({"linux-x64":{"patch":"df8cca6e61abec1438f96b74cf415a1609caef3868ad2e0ebe0a8c20bd571b87","bwrap":"035ac5ddfc70dd7d4a9b575b351944d47aa9d0bbafeb433fb72b2afb02cef7f4","seccomp":"38112f9cf994bed2ed962fae63e803c41874732ab032088b2c1ea3d713e51c11"}});
@@ -0,0 +1,17 @@
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): {
9
+ patch: (path: string) => string;
10
+ linux: (policy: LocalSandboxPolicy) => {
11
+ executable: string;
12
+ seccompFilter: Uint8Array<ArrayBufferLike>;
13
+ };
14
+ probeFilter: (bytes: Uint8Array) => string;
15
+ close: () => void;
16
+ };
17
+ export type NativePayloadStaging = ReturnType<typeof createNativePayloadStaging>;
@@ -0,0 +1,49 @@
1
+ import { rmSync } from "node:fs";
2
+ 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;
19
+ try {
20
+ directory ??= privateNativeDirectory(project, probeNativeRunLiveness);
21
+ const path = stageNativeBytes(directory, label, bytes);
22
+ staged.set(label, path);
23
+ return path;
24
+ }
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 });
29
+ }
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
+ };
49
+ }
@@ -1,5 +1,6 @@
1
1
  import { createAssistantMessageEventStream } from "@earendil-works/pi-ai/utils/event-stream";
2
- import { observeProviderStatus } from "@fungi.computer/mule";
2
+ import { observeProviderStatus, normalizeModel, decodeProviderData, } from "@fungi.computer/mule";
3
+ import { normalizeContext } from "@earendil-works/pi-ai/utils/transcript";
3
4
  import { modelFailureMessage, modelFailureRetryable, } from "./private-provider-rejection.js";
4
5
  import { logEvent } from "./log-event.js";
5
6
  import { localArchiveHandoff } from "./local-archive-handoff.js";
@@ -556,7 +557,7 @@ export function createPrivateLocalModelSource(models, providerId, modelId, provi
556
557
  // prose that any SDK can supply.
557
558
  let modelFailure;
558
559
  return Promise.resolve(Object.freeze({
559
- model: selected,
560
+ model: normalizeModel(selected),
560
561
  retryable: (message) => !signal.aborted &&
561
562
  message.stopReason === "error" &&
562
563
  modelFailure?.message === message &&
@@ -598,13 +599,19 @@ export function createPrivateLocalModelSource(models, providerId, modelId, provi
598
599
  };
599
600
  void (async () => {
600
601
  try {
601
- const upstream = turn.models.streamSimple(selected, context, {
602
- ...streamOptions,
602
+ const { onPayload, onResponse, ...rest } = streamOptions;
603
+ const requestOptions = {
604
+ ...rest,
603
605
  fetch: provider.fetch,
604
- onProviderStreamEvent: provider.onProviderStreamEvent,
606
+ onProviderStreamEvent: (data, model) => provider.onProviderStreamEvent(decodeProviderData(data), normalizeModel(model)),
605
607
  sessionId: providerCacheKey,
606
608
  transport: options?.transport ?? "auto",
607
- });
609
+ };
610
+ if (onPayload !== undefined)
611
+ requestOptions.onPayload = (data, model) => onPayload(decodeProviderData(data), normalizeModel(model));
612
+ if (onResponse !== undefined)
613
+ requestOptions.onResponse = (response, model) => onResponse(response, normalizeModel(model));
614
+ const upstream = turn.models.streamSimple(selected, normalizeContext(context), requestOptions);
608
615
  for await (const event of upstream) {
609
616
  if (event.type === "error") {
610
617
  const error = failure(event.error.usage, event.reason === "aborted", event.error);