@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
@@ -7,6 +7,15 @@ import { RE2JS } from "re2js";
7
7
  import { createPrivateFilesPatch } from "./private-files-patch.js";
8
8
  import { fitLocalImage, maximumLocalImageInputBytes, } from "./local-image-fit.js";
9
9
  import writeFileAtomic from "write-file-atomic";
10
+ const readOnlyFilesMessage = "Read-only mode: switch to Workspace to edit";
11
+ /** An expected Files refusal, not a proposal for an approval. */
12
+ class ReadOnlyFilesRefusal extends FilesEditError {
13
+ constructor(path) {
14
+ super("read-only", path, readOnlyFilesMessage);
15
+ this.name = "ReadOnlyFilesRefusal";
16
+ this.message = readOnlyFilesMessage;
17
+ }
18
+ }
10
19
  const maximumFileBytes = 128 * 1_024;
11
20
  const defaultScanBounds = {
12
21
  candidates: 50_000,
@@ -472,10 +481,14 @@ function createWriteQueue() {
472
481
  * in-root symlinks. This is trusted same-user containment, not protection from
473
482
  * a same-user process racing pathname resolution.
474
483
  */
475
- export async function createPrivateInvocationFilesPort(path, scanBounds = defaultScanBounds, sandbox) {
484
+ export async function createPrivateInvocationFilesPort(path, scanBounds = defaultScanBounds, sandbox, mode = () => "full-access") {
476
485
  const root = await InvocationRoot.capture(path);
477
486
  const write = createWriteQueue();
478
487
  const port = {
488
+ assertEditable(requested) {
489
+ if (mode() === "read-only")
490
+ throw new ReadOnlyFilesRefusal(requested);
491
+ },
479
492
  async read(input, context) {
480
493
  return await readPage(await root.existing(input.path, context), input.offset, input.limit, context);
481
494
  },
@@ -491,6 +504,7 @@ export async function createPrivateInvocationFilesPort(path, scanBounds = defaul
491
504
  async replace(input, context) {
492
505
  return await write(async () => {
493
506
  checkCancellation(context);
507
+ port.assertEditable?.(input.path);
494
508
  const path = await root.existing(input.path, context);
495
509
  const current = await readBoundedText(path, context);
496
510
  if (current !== input.expectedContent)
@@ -500,7 +514,7 @@ export async function createPrivateInvocationFilesPort(path, scanBounds = defaul
500
514
  throw new FilesEditError("file-too-large", input.path, `Result exceeds ${String(maximumFileBytes)} bytes; use files.patch for a small change`);
501
515
  }
502
516
  checkCancellation(context);
503
- sandbox?.assertWrite(path.resolved);
517
+ sandbox?.assertWrite([path.resolved]);
504
518
  await writeFileAtomic(path.resolved, input.content, {
505
519
  encoding: "utf8",
506
520
  });
@@ -515,6 +529,11 @@ export async function createPrivateInvocationFilesPort(path, scanBounds = defaul
515
529
  return Object.freeze(port);
516
530
  return Object.freeze({
517
531
  ...port,
518
- patch: (input, context) => write(() => patch(input, context)),
532
+ patch: (input, context) => write(async () => {
533
+ checkCancellation(context);
534
+ if (mode() === "read-only")
535
+ throw new ReadOnlyFilesRefusal(root.canonicalPath);
536
+ return patch(input, context);
537
+ }),
519
538
  });
520
539
  }
@@ -70,7 +70,7 @@ export declare function createPrivateLocalAgentControl(input: Readonly<{
70
70
  list: (signal: AbortSignal, options?: Readonly<{
71
71
  archived?: boolean;
72
72
  }>) => Promise<readonly AgentControlAgent[]>;
73
- send?: (input: import("@fungi.computer/shiitake/agent-control").AgentDelegationInput, sourceSessionId: string, signal: AbortSignal, invocation?: import("@fungi.computer/shiitake/agent-control").AgentDelegationInvocation) => Promise<import("@fungi.computer/shiitake").Accepted>;
73
+ send?: (input: import("@fungi.computer/shiitake/agent-control").AgentDelegationInput, sourceSessionId: string, signal: AbortSignal, invocation?: import("@fungi.computer/shiitake/agent-control").AgentDelegationInvocation) => Promise<import("@fungi.computer/shiitake").Accepted | import("@fungi.computer/shiitake").Held>;
74
74
  recall?: (input: import("@fungi.computer/shiitake/agent-control").AgentRecallInput, sourceSessionId: string, signal: AbortSignal) => Promise<import("@fungi.computer/shiitake").CancelResult>;
75
75
  create?: (input: Readonly<{
76
76
  requestId: string;
@@ -91,7 +91,7 @@ export declare function createPrivateLocalAgentControl(input: Readonly<{
91
91
  models?: (signal: AbortSignal) => Promise<readonly import("@fungi.computer/shiitake/agent-control").AgentControlModel[]>;
92
92
  checkModel?: (input: Readonly<{
93
93
  agentId: string;
94
- sessionId: string;
94
+ sessionId?: string;
95
95
  }>, signal: AbortSignal) => Promise<void>;
96
96
  open: (agentId: string, signal: AbortSignal) => Promise<Readonly<{
97
97
  agent: import("@fungi.computer/shiitake").ShiitakeAgent;
@@ -1,13 +1,23 @@
1
1
  export declare class LocalBwrapUnavailable extends Error {
2
- readonly reason: "missing" | "invalid-digest" | "digest-mismatch" | "not-executable";
2
+ readonly reason: "install";
3
3
  readonly name = "LocalBwrapUnavailable";
4
- constructor(reason: "missing" | "invalid-digest" | "digest-mismatch" | "not-executable", message: string, options?: ErrorOptions);
4
+ constructor(reason: "install", message: string, options?: ErrorOptions);
5
5
  }
6
6
  export type LinuxSandboxPayload = {
7
- readonly executable: string;
8
- readonly seccompFilter: Uint8Array;
7
+ readonly sourcePath: string;
8
+ readonly executableBytes: Uint8Array;
9
+ readonly seccomp: Uint8Array;
10
+ readonly seccompWorkspace: Uint8Array;
9
11
  };
10
- /** Check both packaged artifacts on every use; never cache verification. */
11
- export declare function verifyLocalLinuxSandboxPayload(executable: string): LinuxSandboxPayload;
12
+ export type LinuxSandboxDigests = Readonly<{
13
+ bwrap: string;
14
+ seccomp: string;
15
+ seccompWorkspace: string;
16
+ }>;
17
+ /** Packaged patch has no sidecar; the built module owns its digest. */
18
+ export type NativeSourceReader = (path: string) => Buffer;
19
+ export declare function verifiedNativePatchHelper(path: string, project: string, readNativeSource?: NativeSourceReader): Uint8Array;
20
+ /** Verify the three packaged Linux files once before copying into service-owned staging. */
21
+ export declare function verifyLocalLinuxSandboxPayload(executable: string, project: string, expected: LinuxSandboxDigests, readNativeSource?: NativeSourceReader): LinuxSandboxPayload;
12
22
  /** Same optional native-package lookup as apply_patch, with no PATH fallback. */
13
- export declare function localLinuxSandboxPayload(): LinuxSandboxPayload;
23
+ export declare function localLinuxSandboxPayload(project: string, readNativeSource?: NativeSourceReader): 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,98 @@ export class LocalBwrapUnavailable extends Error {
16
13
  this.reason = reason;
17
14
  }
18
15
  }
19
- /** 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
- }
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("install", "No built native patch digest for this platform; reinstall Fungi.");
20
+ return verifiedBytes(path, "fungi-apply-patch", project, expected, readNativeSource);
21
+ }
22
+ /** Verify the three packaged Linux files once before copying into service-owned staging. */
23
+ export function verifyLocalLinuxSandboxPayload(executable, project, expected, readNativeSource = readFileSync) {
24
+ const executableBytes = verifiedBytes(executable, "fungi-bwrap", project, expected.bwrap, readNativeSource);
28
25
  return {
29
- executable,
30
- seccompFilter: verifiedBytes(join(dirname(executable), "fungi-seccomp.bpf"), "fungi-seccomp.bpf"),
26
+ sourcePath: executable,
27
+ executableBytes,
28
+ seccomp: verifiedBytes(join(dirname(executable), "fungi-seccomp.bpf"), "fungi-seccomp.bpf", project, expected.seccomp, readNativeSource),
29
+ seccompWorkspace: verifiedBytes(join(dirname(executable), "fungi-seccomp-workspace.bpf"), "fungi-seccomp-workspace.bpf", project, expected.seccompWorkspace, readNativeSource),
31
30
  };
32
31
  }
33
- function verifiedBytes(path, label) {
32
+ function verifiedBytes(path, label, project, expected, readNativeSource = readFileSync) {
34
33
  let bytes;
35
- let digest;
36
34
  try {
35
+ assertHostOwnedNativeFile(project, path);
37
36
  if (!statSync(path).isFile())
38
- throw new LocalBwrapUnavailable("not-executable", `Packaged ${label} is not a regular file; reinstall Fungi.`);
39
- bytes = readFileSync(path);
40
- digest = readFileSync(`${path}.sha256`, "utf8");
37
+ throw new LocalBwrapUnavailable("install", `Packaged ${label} is not a regular file; reinstall Fungi.`);
38
+ bytes = readNativeSource(path);
41
39
  }
42
40
  catch (cause) {
43
41
  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;
42
+ throw new LocalBwrapUnavailable("install", `Packaged ${label} is missing; reinstall Fungi with optional dependencies enabled.`, { cause });
43
+ if (cause instanceof UnsafeNativePayload ||
44
+ cause instanceof LocalBwrapUnavailable)
45
+ throw cause;
46
+ throw new LocalBwrapUnavailable("install", `Packaged ${label} cannot be read; reinstall Fungi. No command was started.`, { cause });
46
47
  }
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)
51
- throw new LocalBwrapUnavailable("digest-mismatch", `Packaged ${label} digest mismatch; reinstall Fungi. No command was started.`);
48
+ if (createHash("sha256").update(bytes).digest("hex") !== expected)
49
+ throw new LocalBwrapUnavailable("install", `Packaged ${label} digest mismatch; reinstall Fungi. No command was started.`);
52
50
  return bytes;
53
51
  }
54
52
  /** Same optional native-package lookup as apply_patch, with no PATH fallback. */
55
- export function localLinuxSandboxPayload() {
53
+ export function localLinuxSandboxPayload(project, readNativeSource = readFileSync) {
54
+ const digest = nativeDigests[`${process.platform}-${process.arch}`];
55
+ if (digest?.bwrap === undefined ||
56
+ digest.seccomp === undefined ||
57
+ digest.seccompWorkspace === undefined)
58
+ throw new LocalBwrapUnavailable("install", "No built native Linux payload digest for this platform; reinstall Fungi.");
56
59
  let executable;
57
60
  if (process.env.FUNGI_BUNDLED_CLI) {
58
61
  const name = `@fungi.computer/spore-native-${process.platform}-${process.arch}`;
62
+ const message = `Packaged ${name}/bin/fungi-bwrap cannot be read; reinstall Fungi.`;
63
+ let directories;
64
+ try {
65
+ directories = createRequire(process.env.FUNGI_BUNDLED_CLI).resolve.paths(name);
66
+ }
67
+ catch (cause) {
68
+ throw new LocalBwrapUnavailable("install", message, { cause });
69
+ }
70
+ if (directories === null)
71
+ throw new LocalBwrapUnavailable("install", message);
72
+ let packageRoot;
73
+ for (const directory of directories) {
74
+ const candidate = join(directory, name);
75
+ try {
76
+ if (lstatSync(candidate, { throwIfNoEntry: false }) !== undefined) {
77
+ packageRoot = candidate;
78
+ break;
79
+ }
80
+ }
81
+ catch (cause) {
82
+ throw new LocalBwrapUnavailable("install", message, { cause });
83
+ }
84
+ }
85
+ if (packageRoot === undefined)
86
+ throw new LocalBwrapUnavailable("install", `Missing ${name}; reinstall Fungi with optional dependencies enabled.`);
87
+ // Package metadata (including exports) is not an authority for whether
88
+ // native bytes are absent. Check the package's location before its bin.
59
89
  try {
60
- executable = createRequire(process.env.FUNGI_BUNDLED_CLI).resolve(`${name}/bin/fungi-bwrap`);
90
+ assertHostOwnedNativeFile(project, packageRoot);
91
+ if (!statSync(packageRoot).isDirectory())
92
+ throw new LocalBwrapUnavailable("install", message);
61
93
  }
62
94
  catch (cause) {
63
- throw new LocalBwrapUnavailable("missing", `Missing ${name}/bin/fungi-bwrap; reinstall Fungi with optional dependencies enabled.`, { cause });
95
+ if (cause instanceof UnsafeNativePayload ||
96
+ cause instanceof LocalBwrapUnavailable)
97
+ throw cause;
98
+ throw new LocalBwrapUnavailable("install", message, { cause });
64
99
  }
100
+ executable = join(packageRoot, "bin", "fungi-bwrap");
65
101
  }
66
102
  else {
67
103
  executable = fileURLToPath(new URL("../native/bin/fungi-bwrap", import.meta.url));
68
104
  }
69
- return verifyLocalLinuxSandboxPayload(executable);
105
+ return verifyLocalLinuxSandboxPayload(executable, project, {
106
+ bwrap: digest.bwrap,
107
+ seccomp: digest.seccomp,
108
+ seccompWorkspace: digest.seccompWorkspace,
109
+ }, readNativeSource);
70
110
  }
@@ -128,11 +128,17 @@ export declare function createPrivateLocalComputerProcesses(root: string, home?:
128
128
  */
129
129
  export declare const backgroundExecInput: z.ZodUnion<readonly [z.ZodObject<{
130
130
  operation: z.ZodLiteral<"start">;
131
+ escape: z.ZodOptional<z.ZodObject<{
132
+ justification: z.ZodString;
133
+ }, z.core.$strict>>;
131
134
  command: z.ZodString;
132
135
  stdin: z.ZodOptional<z.ZodString>;
133
136
  cwd: z.ZodOptional<z.ZodString>;
134
137
  }, z.core.$strict>, z.ZodObject<{
135
138
  operation: z.ZodLiteral<"start">;
139
+ escape: z.ZodOptional<z.ZodObject<{
140
+ justification: z.ZodString;
141
+ }, z.core.$strict>>;
136
142
  executable: z.ZodString;
137
143
  args: z.ZodDefault<z.ZodArray<z.ZodString>>;
138
144
  stdin: z.ZodOptional<z.ZodString>;
@@ -178,6 +184,15 @@ export declare const backgroundExecOutput: z.ZodUnion<readonly [z.ZodObject<{
178
184
  complete: z.ZodBoolean;
179
185
  timedOut: z.ZodBoolean;
180
186
  signalled: z.ZodOptional<z.ZodBoolean>;
187
+ sandbox: z.ZodOptional<z.ZodObject<{
188
+ mode: z.ZodEnum<{
189
+ "read-only": "read-only";
190
+ workspace: "workspace";
191
+ "full-access": "full-access";
192
+ }>;
193
+ likelyDenied: z.ZodBoolean;
194
+ ranOutsideSandbox: z.ZodBoolean;
195
+ }, z.core.$strict>>;
181
196
  }, z.core.$strict>, z.ZodObject<{
182
197
  processes: z.ZodArray<z.ZodObject<{
183
198
  id: z.ZodString & z.ZodType<import("@fungi.computer/cairn").CairnId<"local-computer-process", "process", 1>, string, z.core.$ZodTypeInternals<import("@fungi.computer/cairn").CairnId<"local-computer-process", "process", 1>, string>>;
@@ -65,6 +65,9 @@ const processSchema = z.strictObject({
65
65
  /** The intentional shell form of background `start`. */
66
66
  const shellStartInput = z.strictObject({
67
67
  operation: z.literal("start"),
68
+ escape: z
69
+ .strictObject({ justification: z.string().min(1).max(2048) })
70
+ .optional(),
68
71
  command: z.string().min(1).max(maximumLaunchTextLength),
69
72
  stdin: launchStdinSchema.optional(),
70
73
  cwd: launchCwdSchema.optional(),
@@ -73,6 +76,9 @@ const shellStartInput = z.strictObject({
73
76
  const directStartInput = z
74
77
  .strictObject({
75
78
  operation: z.literal("start"),
79
+ escape: z
80
+ .strictObject({ justification: z.string().min(1).max(2048) })
81
+ .optional(),
76
82
  executable: z.string().min(1),
77
83
  args: z.array(z.string()).default([]),
78
84
  stdin: launchStdinSchema.optional(),
@@ -573,6 +579,13 @@ export const backgroundExecInput = z.union([
573
579
  const backgroundResult = readOutput.extend({
574
580
  timedOut: z.boolean(),
575
581
  signalled: z.boolean().optional(),
582
+ sandbox: z
583
+ .strictObject({
584
+ mode: z.enum(["full-access", "workspace", "read-only"]),
585
+ likelyDenied: z.boolean(),
586
+ ranOutsideSandbox: z.boolean(),
587
+ })
588
+ .optional(),
576
589
  });
577
590
  export const backgroundExecOutput = z.union([backgroundResult, listOutput]);
578
591
  /** Reuse Session ownership and logs; a wait deadline never cancels the command. */
@@ -1,13 +1,12 @@
1
+ import { type NativePayloadStaging } from "./private-local-native-staging.js";
1
2
  import { z } from "zod";
3
+ import type { LocalPermissionMode } from "./private-local-permission-mode.js";
2
4
  export declare const sandboxUnavailableReasonSchema: z.ZodObject<{
3
5
  kind: z.ZodEnum<{
4
- missing: "missing";
5
- "invalid-digest": "invalid-digest";
6
- "digest-mismatch": "digest-mismatch";
7
- "not-executable": "not-executable";
8
- "unsupported-platform": "unsupported-platform";
9
- "invalid-folder": "invalid-folder";
10
- "backend-refused": "backend-refused";
6
+ install: "install";
7
+ unsupported: "unsupported";
8
+ folder: "folder";
9
+ "startup-refused": "startup-refused";
11
10
  }>;
12
11
  message: z.ZodString;
13
12
  }, z.core.$strict>;
@@ -22,13 +21,14 @@ export type SandboxCommand = Readonly<{
22
21
  args: readonly string[];
23
22
  environment: Readonly<{
24
23
  TMPDIR: string;
24
+ SSH_AUTH_SOCK?: string;
25
25
  }>;
26
26
  /** Linux only: feed these verified bytes to fd3; never inherit arbitrary fds. */
27
27
  seccompFilter?: Uint8Array;
28
28
  }>;
29
29
  /** Existing foreground/background owners retain process and cancellation lifetime. */
30
30
  export type LocalSandbox = Readonly<{
31
- command(cwd: string, argv: readonly [string, ...string[]]): SandboxCommand;
31
+ command(cwd: string, argv: readonly [string, ...string[]], readOnlyHelperFile?: string): SandboxCommand;
32
32
  close(): void;
33
33
  }>;
34
34
  export type SandboxAdmission = Readonly<{
@@ -38,6 +38,7 @@ export type SandboxAdmission = Readonly<{
38
38
  state: "unavailable";
39
39
  reason: SandboxUnavailableReason;
40
40
  }>;
41
+ export declare function sandboxUnavailableReason(cause: unknown): SandboxUnavailableReason;
41
42
  /**
42
43
  * The caller already owns the person's auto-approve choice. This module has no
43
44
  * mode, setting, bare-command branch, PATH fallback or permission to widen it.
@@ -47,5 +48,14 @@ export declare function establishLocalSandbox(input: Readonly<{
47
48
  project: string;
48
49
  /** Absolute folder selected by the Local owner. */
49
50
  folder: string;
51
+ mode: Exclude<LocalPermissionMode, "full-access">;
52
+ temporaryDirectory: string;
50
53
  platform?: string;
54
+ }>, staging?: NativePayloadStaging,
55
+ /** Host startup probe; a Linux command-generation test can supply its own runner. */
56
+ runProbe?: (command: SandboxCommand) => Readonly<{
57
+ status: number | null;
58
+ stderr: string;
59
+ signal: string | null;
60
+ error?: Error;
51
61
  }>): SandboxAdmission;
@@ -1,22 +1,14 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import { closeSync, mkdtempSync, openSync, realpathSync, rmSync, writeFileSync, } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import { LocalBwrapUnavailable, localLinuxSandboxPayload, } from "./private-local-bwrap.js";
2
+ import { closeSync, openSync, realpathSync } from "node:fs";
3
+ import { userInfo } from "node:os";
4
+ import { LocalBwrapUnavailable } from "./private-local-bwrap.js";
5
+ import { createNativePayloadStaging, } from "./private-local-native-staging.js";
6
6
  import { linuxSandboxCommand } from "./private-local-linux-sandbox.js";
7
- import { seatbeltCommand } from "./private-local-seatbelt.js";
8
- import { sandboxPolicy } from "./private-local-sandbox-policy.js";
7
+ import { seatbeltCommand, UnsafeSeatbeltFolder, } from "./private-local-seatbelt.js";
8
+ import { sandboxPolicy, linuxSandboxProfile, UnsafeGitMarker, UnsafeHostCode, UnsafeNativePayload, } from "./private-local-sandbox-policy.js";
9
9
  import { z } from "zod";
10
10
  export const sandboxUnavailableReasonSchema = z.strictObject({
11
- kind: z.enum([
12
- "unsupported-platform",
13
- "invalid-folder",
14
- "missing",
15
- "invalid-digest",
16
- "digest-mismatch",
17
- "not-executable",
18
- "backend-refused",
19
- ]),
11
+ kind: z.enum(["unsupported", "install", "folder", "startup-refused"]),
20
12
  message: z.string(),
21
13
  });
22
14
  export class LocalSandboxUnavailable extends Error {
@@ -27,11 +19,19 @@ export class LocalSandboxUnavailable extends Error {
27
19
  this.reason = reason;
28
20
  }
29
21
  }
30
- function unavailable(cause) {
22
+ export function sandboxUnavailableReason(cause) {
23
+ if (cause instanceof UnsafeGitMarker)
24
+ return { kind: "folder", message: cause.message };
25
+ if (cause instanceof UnsafeSeatbeltFolder)
26
+ return { kind: "folder", message: cause.message };
27
+ if (cause instanceof UnsafeHostCode)
28
+ return { kind: "install", message: cause.message };
29
+ if (cause instanceof UnsafeNativePayload)
30
+ return { kind: "install", message: cause.message };
31
31
  if (cause instanceof LocalBwrapUnavailable)
32
32
  return { kind: cause.reason, message: cause.message };
33
33
  return {
34
- kind: "backend-refused",
34
+ kind: "startup-refused",
35
35
  message: cause instanceof Error ? cause.message : "Sandbox preparation failed.",
36
36
  };
37
37
  }
@@ -40,7 +40,9 @@ function unavailable(cause) {
40
40
  * mode, setting, bare-command branch, PATH fallback or permission to widen it.
41
41
  * A ready result proves only that the backend can start, not the escape matrix.
42
42
  */
43
- export function establishLocalSandbox(input) {
43
+ export function establishLocalSandbox(input, staging,
44
+ /** Host startup probe; a Linux command-generation test can supply its own runner. */
45
+ runProbe) {
44
46
  try {
45
47
  sandboxPolicy(input.project, input.folder);
46
48
  }
@@ -48,7 +50,7 @@ export function establishLocalSandbox(input) {
48
50
  return {
49
51
  state: "unavailable",
50
52
  reason: {
51
- kind: "invalid-folder",
53
+ kind: "folder",
52
54
  message: cause instanceof Error ? cause.message : "Invalid sandbox folder.",
53
55
  },
54
56
  };
@@ -58,44 +60,66 @@ export function establishLocalSandbox(input) {
58
60
  return {
59
61
  state: "unavailable",
60
62
  reason: {
61
- kind: "unsupported-platform",
62
- message: `Local sandbox is unavailable on ${platform}.`,
63
+ kind: "unsupported",
64
+ message: `${input.mode === "workspace" ? "Workspace" : "Read-only"} on ${platform} needs a supported OS sandbox; use --yolo or run on macOS/Linux.`,
63
65
  },
64
66
  };
65
- let temporaryDirectory;
67
+ let native;
66
68
  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;
69
+ native =
70
+ staging ??
71
+ (platform === "linux"
72
+ ? createNativePayloadStaging(input.project)
73
+ : undefined);
74
+ const environmentTmp = platform === "linux" ? "/tmp" : input.temporaryDirectory;
70
75
  let closed = false;
71
76
  const sandbox = {
72
- command: (cwd, argv) => {
77
+ command: (cwd, argv, readOnlyHelperFile) => {
73
78
  if (closed)
74
79
  throw new LocalSandboxUnavailable({
75
- kind: "backend-refused",
80
+ kind: "startup-refused",
76
81
  message: "Sandbox has been closed.",
77
82
  });
78
83
  // Failed/incomplete authority discovery is not an unavailable backend:
79
84
  // it must not turn into an authorized unsandboxed retry.
80
85
  const policy = sandboxPolicy(input.project, input.folder);
81
86
  try {
82
- const launch = platform === "linux"
83
- ? linuxSandboxCommand(policy, localLinuxSandboxPayload(), cwd, argv)
84
- : seatbeltCommand({ ...policy, temporaryDirectory: temporary }, argv);
85
- return {
86
- ...launch,
87
- environment: { TMPDIR: platform === "linux" ? "/tmp" : temporary },
87
+ const profile = linuxSandboxProfile(policy, input.mode, input.temporaryDirectory, process.env, userInfo().homedir, platform);
88
+ const linuxPayload = platform === "linux" ? native?.linux(profile) : undefined;
89
+ const stagedHelper = readOnlyHelperFile === undefined
90
+ ? undefined
91
+ : native?.patch(readOnlyHelperFile);
92
+ const stagedArgv = stagedHelper === undefined
93
+ ? argv
94
+ : [
95
+ argv[0] === readOnlyHelperFile ? stagedHelper : argv[0],
96
+ ...argv
97
+ .slice(1)
98
+ .map((argument) => argument === readOnlyHelperFile ? stagedHelper : argument),
99
+ ];
100
+ const launch = linuxPayload !== undefined
101
+ ? linuxSandboxCommand(profile, linuxPayload, cwd, stagedArgv, undefined, stagedHelper)
102
+ : seatbeltCommand(profile, stagedArgv);
103
+ let environment = {
104
+ TMPDIR: environmentTmp,
88
105
  };
106
+ if (platform === "linux" && profile.sshAgentSocket !== undefined)
107
+ environment = {
108
+ TMPDIR: environmentTmp,
109
+ SSH_AUTH_SOCK: profile.sshAgentSocket,
110
+ };
111
+ return { ...launch, environment };
89
112
  }
90
113
  catch (cause) {
91
- throw new LocalSandboxUnavailable(unavailable(cause));
114
+ throw new LocalSandboxUnavailable(sandboxUnavailableReason(cause));
92
115
  }
93
116
  },
94
117
  close: () => {
95
118
  if (closed)
96
119
  return;
97
120
  closed = true;
98
- rmSync(temporary, { recursive: true });
121
+ if (staging === undefined)
122
+ native?.close();
99
123
  },
100
124
  };
101
125
  const command = sandbox.command(realpathSync(input.folder), [
@@ -104,8 +128,9 @@ export function establishLocalSandbox(input) {
104
128
  let filterDescriptor;
105
129
  const probe = () => {
106
130
  if (command.seccompFilter !== undefined) {
107
- const path = join(temporary, "seccomp.bpf");
108
- writeFileSync(path, command.seccompFilter, { mode: 0o600 });
131
+ const path = native?.probeFilter(command.seccompFilter);
132
+ if (path === undefined)
133
+ throw new Error("No Linux filter staging owner");
109
134
  filterDescriptor = openSync(path, "r");
110
135
  }
111
136
  return spawnSync(command.executable, [...command.args], {
@@ -125,7 +150,7 @@ export function establishLocalSandbox(input) {
125
150
  };
126
151
  const result = (() => {
127
152
  try {
128
- return probe();
153
+ return runProbe?.(command) ?? probe();
129
154
  }
130
155
  finally {
131
156
  if (filterDescriptor !== undefined)
@@ -134,28 +159,28 @@ export function establishLocalSandbox(input) {
134
159
  })();
135
160
  if (result.status !== 0) {
136
161
  sandbox.close();
137
- const stderr = result.stderr.trim();
162
+ const stderr = result.error === undefined ? result.stderr.trim() : "";
138
163
  return {
139
164
  state: "unavailable",
140
165
  reason: {
141
- kind: "backend-refused",
142
- message: result.error?.message ??
166
+ kind: "startup-refused",
167
+ message: `${input.mode === "workspace" ? "Workspace" : "Read-only"} on this ${platform === "darwin" ? "Mac" : "Linux host"} needs a working ${platform === "darwin" ? "sandbox-exec profile probe" : "bwrap installation"}; use --yolo or ${platform === "darwin" ? "check the failed Seatbelt profile probe" : "fix the sandbox installation"}. ${result.error?.message ??
143
168
  (stderr.length > 0
144
169
  ? stderr
145
- : `Sandbox startup exited ${String(result.status)} (${String(result.signal)}).`),
170
+ : `Sandbox startup exited ${String(result.status)} (${String(result.signal)}).`)}`,
146
171
  },
147
172
  };
148
173
  }
149
174
  return { state: "ready", sandbox };
150
175
  }
151
176
  catch (cause) {
152
- if (temporaryDirectory !== undefined)
153
- rmSync(temporaryDirectory, { recursive: true });
177
+ if (staging === undefined)
178
+ native?.close();
154
179
  return {
155
180
  state: "unavailable",
156
181
  reason: cause instanceof LocalSandboxUnavailable
157
182
  ? cause.reason
158
- : unavailable(cause),
183
+ : sandboxUnavailableReason(cause),
159
184
  };
160
185
  }
161
186
  }
@@ -1,5 +1,5 @@
1
1
  import type { BirdDogMail } from "@fungi.computer/bird-dog";
2
- import type { Accepted, CancelResult, CommandSettlement, VerifiedAgentReply } from "@fungi.computer/shiitake";
2
+ import type { Accepted, CancelResult, CommandSettlement, DelegationReceipt, VerifiedAgentReply } from "@fungi.computer/shiitake";
3
3
  import type { AgentControlDirectory, AgentDelegationInput, AgentDelegationInvocation, AgentRecallInput } from "@fungi.computer/shiitake/agent-control";
4
4
  import { createFleetDelegation } from "@fungi.computer/shiitake/fleet";
5
5
  /**
@@ -8,7 +8,7 @@ import { createFleetDelegation } from "@fungi.computer/shiitake/fleet";
8
8
  * owner when it is idle; `readSettlement` reads one exact command there.
9
9
  */
10
10
  export type PrivateLocalMailRouting = Readonly<{
11
- deliver: (mail: BirdDogMail) => Promise<Accepted>;
11
+ deliver: (mail: BirdDogMail) => Promise<DelegationReceipt>;
12
12
  readSettlement: (agentId: string, sessionId: string, commandId: string) => Promise<CommandSettlement | undefined>;
13
13
  }>;
14
14
  /**
@@ -29,7 +29,7 @@ export declare function createPrivateLocalDelegation(input: Readonly<{
29
29
  }[] | undefined>) | undefined;
30
30
  accept: (mail: BirdDogMail, meta?: BirdDogMail["meta"], reply?: VerifiedAgentReply) => Promise<Accepted>;
31
31
  }>): {
32
- send: (task: AgentDelegationInput, sourceSessionId: string, signal: AbortSignal, invocation?: AgentDelegationInvocation) => Promise<Accepted>;
32
+ send: (task: AgentDelegationInput, sourceSessionId: string, signal: AbortSignal, invocation?: AgentDelegationInvocation) => Promise<DelegationReceipt>;
33
33
  recall: (task: AgentRecallInput, sourceSessionId: string, signal: AbortSignal) => Promise<CancelResult>;
34
34
  receive: (mail: BirdDogMail) => Promise<Accepted>;
35
35
  outstanding: () => Promise<import("@fungi.computer/shiitake/fleet").OutstandingDelegation | undefined>;