@fungi.computer/truffle 0.1.1 → 0.2.1

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 (174) hide show
  1. package/README.md +282 -34
  2. package/dist/auth-cli.d.ts +10 -0
  3. package/dist/auth-cli.js +36 -14
  4. package/dist/fungi-account-command.d.ts +1 -1
  5. package/dist/fungi-account-command.js +6 -3
  6. package/dist/index.d.ts +10 -2
  7. package/dist/index.js +8 -2
  8. package/dist/internal/image-fit-metafile.json +106 -0
  9. package/dist/internal/local-image-fit-worker.mjs +960 -0
  10. package/dist/internal/private-workerd-sandbox-worker.mjs +11 -12
  11. package/dist/internal/sandbox-metafile.json +7 -7
  12. package/dist/local-archive-handoff.d.ts +12 -0
  13. package/dist/local-archive-handoff.js +21 -0
  14. package/dist/local-extensions.d.ts +58 -12
  15. package/dist/local-extensions.js +195 -55
  16. package/dist/local-image-fit.d.ts +30 -0
  17. package/dist/local-image-fit.js +105 -0
  18. package/dist/local-models.d.ts +4 -2
  19. package/dist/local-models.js +11 -6
  20. package/dist/local-session-archives.d.ts +8 -0
  21. package/dist/local-session-archives.js +152 -0
  22. package/dist/local-skill-source.d.ts +18 -3
  23. package/dist/local-skill-source.js +216 -140
  24. package/dist/log-event.d.ts +7 -0
  25. package/dist/log-event.js +8 -4
  26. package/dist/private-bounded-response.d.ts +2 -0
  27. package/dist/private-bounded-response.js +41 -0
  28. package/dist/private-file-credential-store.d.ts +28 -3
  29. package/dist/private-file-credential-store.js +57 -9
  30. package/dist/private-files-patch.d.ts +2 -1
  31. package/dist/private-files-patch.js +21 -3
  32. package/dist/private-fungi-account.d.ts +40 -23
  33. package/dist/private-fungi-account.js +201 -21
  34. package/dist/private-fungi-mcp-connection.d.ts +4 -0
  35. package/dist/private-fungi-mcp-connection.js +25 -6
  36. package/dist/private-fungi-mcp-selection-schema.d.ts +7 -0
  37. package/dist/private-fungi-mcp-selection-schema.js +15 -0
  38. package/dist/private-fungi-mcp-selection.d.ts +6 -6
  39. package/dist/private-fungi-mcp-selection.js +29 -16
  40. package/dist/private-fungi-model-api.js +4 -4
  41. package/dist/private-fungi-model-catalog.d.ts +10 -0
  42. package/dist/private-fungi-model-catalog.js +10 -2
  43. package/dist/private-fungi-model-provider.d.ts +1 -1
  44. package/dist/private-fungi-model-provider.js +6 -1
  45. package/dist/private-fungi-pending-logouts.d.ts +1 -1
  46. package/dist/private-fungi-pending-logouts.js +1 -2
  47. package/dist/private-fungi-service-logout.d.ts +44 -0
  48. package/dist/private-fungi-service-logout.js +59 -0
  49. package/dist/private-invocation-computer.d.ts +24 -3
  50. package/dist/private-invocation-computer.js +101 -62
  51. package/dist/private-invocation-files.d.ts +16 -1
  52. package/dist/private-invocation-files.js +154 -83
  53. package/dist/private-local-acp-stdio.js +3 -0
  54. package/dist/private-local-admission-gate.d.ts +14 -0
  55. package/dist/private-local-admission-gate.js +52 -0
  56. package/dist/private-local-agent-control.d.ts +101 -0
  57. package/dist/private-local-agent-control.js +30 -0
  58. package/dist/private-local-agent-default-model.d.ts +12 -0
  59. package/dist/private-local-agent-default-model.js +31 -0
  60. package/dist/private-local-auth-rpc.d.ts +67 -7
  61. package/dist/private-local-auth-rpc.js +140 -17
  62. package/dist/private-local-bwrap.d.ts +13 -0
  63. package/dist/private-local-bwrap.js +70 -0
  64. package/dist/private-local-client-version.d.ts +5 -0
  65. package/dist/private-local-client-version.js +43 -0
  66. package/dist/private-local-computer-attach.d.ts +3 -3
  67. package/dist/private-local-computer-attach.js +17 -3
  68. package/dist/private-local-computer-processes.d.ts +6 -3
  69. package/dist/private-local-computer-processes.js +55 -31
  70. package/dist/private-local-config-root.d.ts +21 -0
  71. package/dist/private-local-config-root.js +19 -0
  72. package/dist/private-local-containment.d.ts +51 -0
  73. package/dist/private-local-containment.js +161 -0
  74. package/dist/private-local-delegation.d.ts +10 -3
  75. package/dist/private-local-delegation.js +70 -15
  76. package/dist/private-local-effective-model.d.ts +9 -1
  77. package/dist/private-local-effective-model.js +3 -0
  78. package/dist/private-local-exec-environment.d.ts +16 -0
  79. package/dist/private-local-exec-environment.js +11 -0
  80. package/dist/private-local-linux-sandbox.d.ts +8 -0
  81. package/dist/private-local-linux-sandbox.js +67 -0
  82. package/dist/private-local-mail.d.ts +4 -2
  83. package/dist/private-local-mail.js +41 -33
  84. package/dist/private-local-mesh-enrollment.d.ts +34 -0
  85. package/dist/private-local-mesh-enrollment.js +188 -0
  86. package/dist/private-local-mesh-links.d.ts +21 -0
  87. package/dist/private-local-mesh-links.js +229 -0
  88. package/dist/private-local-mesh-record.d.ts +120 -0
  89. package/dist/private-local-mesh-record.js +55 -0
  90. package/dist/private-local-process-identity.d.ts +20 -0
  91. package/dist/private-local-process-identity.js +40 -0
  92. package/dist/private-local-runtime-host.d.ts +64 -13
  93. package/dist/private-local-runtime-host.js +361 -172
  94. package/dist/private-local-sandbox-policy.d.ts +21 -0
  95. package/dist/private-local-sandbox-policy.js +121 -0
  96. package/dist/private-local-sandbox.d.ts +37 -0
  97. package/dist/private-local-sandbox.js +114 -0
  98. package/dist/private-local-seatbelt.d.ts +24 -0
  99. package/dist/private-local-seatbelt.js +68 -0
  100. package/dist/private-local-service-contract.d.ts +91 -11
  101. package/dist/private-local-service-discovery.d.ts +129 -13
  102. package/dist/private-local-service-discovery.js +148 -47
  103. package/dist/private-local-service-lease.d.ts +16 -0
  104. package/dist/private-local-service-lease.js +105 -0
  105. package/dist/private-local-service-listener.d.ts +9 -2
  106. package/dist/private-local-service-listener.js +260 -64
  107. package/dist/private-local-service-log.d.ts +128 -0
  108. package/dist/private-local-service-log.js +321 -0
  109. package/dist/private-local-service-owner.d.ts +87 -1519
  110. package/dist/private-local-service-owner.js +1069 -71
  111. package/dist/private-local-service-process.js +3 -1
  112. package/dist/private-local-service-termination.d.ts +7 -0
  113. package/dist/private-local-service-termination.js +67 -0
  114. package/dist/private-local-service.d.ts +31 -2
  115. package/dist/private-local-service.js +402 -126
  116. package/dist/private-local-session-discovery.d.ts +52 -5
  117. package/dist/private-local-session-discovery.js +132 -11
  118. package/dist/private-local-shiitake-host.js +2 -2
  119. package/dist/private-local-startup-model.d.ts +51 -0
  120. package/dist/private-local-startup-model.js +86 -0
  121. package/dist/private-local-stop-all.d.ts +27 -0
  122. package/dist/private-local-stop-all.js +26 -0
  123. package/dist/private-local-ui-attachment.d.ts +19 -0
  124. package/dist/private-local-ui-attachment.js +71 -0
  125. package/dist/private-local-worker-server.d.ts +1 -0
  126. package/dist/private-local-worker-server.js +13 -5
  127. package/dist/private-mcp-form-contract.d.ts +186 -0
  128. package/dist/private-mcp-form-contract.js +53 -0
  129. package/dist/private-mcp-form-elicitation.d.ts +18 -0
  130. package/dist/private-mcp-form-elicitation.js +97 -0
  131. package/dist/private-mcp-form-session.d.ts +172 -0
  132. package/dist/private-mcp-form-session.js +148 -0
  133. package/dist/private-openai-claims.d.ts +30 -0
  134. package/dist/private-openai-claims.js +69 -0
  135. package/dist/private-oyster-mcp-auth.d.ts +13 -0
  136. package/dist/private-oyster-mcp-auth.js +57 -0
  137. package/dist/private-provider-rejection.d.ts +15 -0
  138. package/dist/private-provider-rejection.js +24 -0
  139. package/dist/private-sqlite-owner.d.ts +34 -2
  140. package/dist/private-sqlite-owner.js +103 -37
  141. package/dist/private-team-computers.d.ts +4 -4
  142. package/dist/private-team-computers.js +11 -2
  143. package/dist/seatbelt/safe-base.sbpl +79 -0
  144. package/dist/subscription-activation.d.ts +9 -0
  145. package/dist/subscription-activation.js +62 -0
  146. package/dist/subscription-identity.d.ts +23 -0
  147. package/dist/subscription-identity.js +124 -0
  148. package/dist/subscription-turn.d.ts +17 -0
  149. package/dist/subscription-turn.js +143 -0
  150. package/dist/team-apps.d.ts +2 -2
  151. package/dist/team-apps.js +7 -2
  152. package/native/bin/LICENSE.bubblewrap +502 -0
  153. package/native/bin/LICENSE.libcap +112 -0
  154. package/native/bin/LICENSE.sandbox-runtime +201 -0
  155. package/native/bin/SOURCE.bubblewrap.tar.xz +0 -0
  156. package/native/bin/bwrap-build-source/Dockerfile +48 -0
  157. package/native/bin/bwrap-build-source/README.md +96 -0
  158. package/native/bin/bwrap-build-source/build.mjs +52 -0
  159. package/native/bin/bwrap-build-source/linux-arm64.ini +14 -0
  160. package/native/bin/bwrap-build-source/pin.mjs +21 -0
  161. package/native/bin/bwrap-build-source/record.mjs +74 -0
  162. package/native/bin/bwrap-build-source/vendor/LICENSE.sandbox-runtime +201 -0
  163. package/native/bin/bwrap-build-source/vendor/README.md +23 -0
  164. package/native/bin/bwrap-build-source/vendor/seccomp-unix-block.c +280 -0
  165. package/native/bin/bwrap-build.json +44 -0
  166. package/native/bin/fungi-bwrap +0 -0
  167. package/native/bin/fungi-bwrap.sha256 +1 -0
  168. package/native/bin/fungi-seccomp.bpf +0 -0
  169. package/native/bin/fungi-seccomp.bpf.sha256 +1 -0
  170. package/package.json +44 -31
  171. package/dist/private-local-model-settings.d.ts +0 -5
  172. package/dist/private-local-model-settings.js +0 -31
  173. package/dist/private-local-subagents.d.ts +0 -27
  174. package/dist/private-local-subagents.js +0 -77
@@ -0,0 +1,70 @@
1
+ import { createHash } from "node:crypto";
2
+ import { accessSync, constants, readFileSync, statSync } from "node:fs";
3
+ import { createRequire } from "node:module";
4
+ import { dirname, join } from "node:path";
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);
11
+ export class LocalBwrapUnavailable extends Error {
12
+ reason;
13
+ name = "LocalBwrapUnavailable";
14
+ constructor(reason, message, options) {
15
+ super(message, options);
16
+ this.reason = reason;
17
+ }
18
+ }
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
+ }
28
+ return {
29
+ executable,
30
+ seccompFilter: verifiedBytes(join(dirname(executable), "fungi-seccomp.bpf"), "fungi-seccomp.bpf"),
31
+ };
32
+ }
33
+ function verifiedBytes(path, label) {
34
+ let bytes;
35
+ let digest;
36
+ try {
37
+ 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");
41
+ }
42
+ catch (cause) {
43
+ 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;
46
+ }
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.`);
52
+ return bytes;
53
+ }
54
+ /** Same optional native-package lookup as apply_patch, with no PATH fallback. */
55
+ export function localLinuxSandboxPayload() {
56
+ let executable;
57
+ if (process.env.FUNGI_BUNDLED_CLI) {
58
+ const name = `@fungi.computer/spore-native-${process.platform}-${process.arch}`;
59
+ try {
60
+ executable = createRequire(process.env.FUNGI_BUNDLED_CLI).resolve(`${name}/bin/fungi-bwrap`);
61
+ }
62
+ catch (cause) {
63
+ throw new LocalBwrapUnavailable("missing", `Missing ${name}/bin/fungi-bwrap; reinstall Fungi with optional dependencies enabled.`, { cause });
64
+ }
65
+ }
66
+ else {
67
+ executable = fileURLToPath(new URL("../native/bin/fungi-bwrap", import.meta.url));
68
+ }
69
+ return verifyLocalLinuxSandboxPayload(executable);
70
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Read the installed producer, never the calling window's frozen environment.
3
+ * Undefined means no fungi is installed on PATH, so no client is current.
4
+ */
5
+ export declare function readInstalledLocalClientBuildId(launcher?: string): Promise<string | undefined>;
@@ -0,0 +1,43 @@
1
+ import { execFile } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import { readFile, realpath } from "node:fs/promises";
4
+ import { basename, dirname, join } from "node:path";
5
+ import { promisify } from "node:util";
6
+ import { z } from "zod";
7
+ const execute = promisify(execFile);
8
+ const queryOptions = { timeout: 2_000, maxBuffer: 4_096 };
9
+ const buildId = z.string().regex(/^[a-f0-9]{64}$/u);
10
+ function installedLauncherOnPath() {
11
+ return new Promise((resolve, reject) => {
12
+ execFile("which", ["fungi"], queryOptions, (error, stdout) => {
13
+ if (error === null)
14
+ resolve(stdout.trim());
15
+ // which exits 1 when nothing named fungi is installed on PATH.
16
+ else if (error.code === 1)
17
+ resolve(undefined);
18
+ else
19
+ reject(new Error("Could not look up the installed fungi launcher", {
20
+ cause: error,
21
+ }));
22
+ });
23
+ });
24
+ }
25
+ /**
26
+ * Read the installed producer, never the calling window's frozen environment.
27
+ * Undefined means no fungi is installed on PATH, so no client is current.
28
+ */
29
+ export async function readInstalledLocalClientBuildId(launcher) {
30
+ const found = launcher ?? (await installedLauncherOnPath());
31
+ if (found === undefined)
32
+ return undefined;
33
+ const installed = await realpath(found);
34
+ if (basename(installed) === "fungi.cjs") {
35
+ // npm replaces cli.js in place. This is the launcher's own identity rule.
36
+ return createHash("sha256")
37
+ .update(await readFile(join(dirname(installed), "cli.js")))
38
+ .digest("hex");
39
+ }
40
+ // Compiled launchers identify their embedded archive, not executable bytes.
41
+ const { stdout } = await execute(installed, ["--internal-build-id"], queryOptions);
42
+ return buildId.parse(JSON.parse(stdout));
43
+ }
@@ -18,7 +18,7 @@ export declare function attachPrivateLocalComputer(rawEndpoint: string | URL, in
18
18
  computer: string;
19
19
  folderPath: string;
20
20
  hostLabel?: string;
21
- }>, options?: ControlOptions): Promise<PrivateLocalComputerAttachResult>;
21
+ }>, controlOptions?: ControlOptions): Promise<PrivateLocalComputerAttachResult>;
22
22
  export type PrivateLocalComputerDetachResult = Readonly<{
23
23
  kind: "detached" | "not-attached" | "uncertain" | "denied";
24
24
  }>;
@@ -26,7 +26,7 @@ export type PrivateLocalComputerDetachResult = Readonly<{
26
26
  * Detach a Computer's attachment on the Hub, then forget it here. A pending
27
27
  * attach is replayed first so the Hub never keeps an attachment Local forgot.
28
28
  */
29
- export declare function detachPrivateLocalComputer(rawEndpoint: string | URL, computer: string, options?: ControlOptions): Promise<PrivateLocalComputerDetachResult>;
29
+ export declare function detachPrivateLocalComputer(rawEndpoint: string | URL, computer: string, input?: ControlOptions): Promise<PrivateLocalComputerDetachResult>;
30
30
  /** What `fungi computer status` shows: never a key. */
31
31
  export declare function listPrivateLocalComputers(options?: Readonly<{
32
32
  credentialPath?: string;
@@ -46,4 +46,4 @@ export type PrivateLocalComputerRunOptions = {
46
46
  * Serve one attached folder until `signal` aborts or the Hub refuses the
47
47
  * machine (it was detached elsewhere), in which case Local forgets it.
48
48
  */
49
- export declare function runPrivateLocalComputer(rawEndpoint: string | URL, computer: string, options: PrivateLocalComputerRunOptions): Promise<"stopped" | "detached">;
49
+ export declare function runPrivateLocalComputer(rawEndpoint: string | URL, computer: string, input: PrivateLocalComputerRunOptions): Promise<"stopped" | "detached">;
@@ -4,6 +4,7 @@ import { hostname } from "node:os";
4
4
  import { basename, resolve } from "node:path";
5
5
  import { defineIdFamily } from "@fungi.computer/cairn/id";
6
6
  import { z } from "zod";
7
+ import { capturePrivateLocalConfigRoot } from "./private-local-config-root.js";
7
8
  import { callFungiMcpTeamControl, fungiMcpTeamEndpoint, readFungiMcpLocalBackends, writeFungiMcpLocalBackends, } from "./private-fungi-account.js";
8
9
  import { runLocalComputerBackend, startLocalComputerd, } from "./private-local-computer-backend.js";
9
10
  /**
@@ -76,7 +77,12 @@ function deviceKey() {
76
77
  };
77
78
  }
78
79
  /** Attach `folderPath` to a Team Computer, or return the existing attachment. */
79
- export async function attachPrivateLocalComputer(rawEndpoint, input, options = {}) {
80
+ export async function attachPrivateLocalComputer(rawEndpoint, input, controlOptions = {}) {
81
+ const options = {
82
+ ...controlOptions,
83
+ credentialPath: controlOptions.credentialPath ??
84
+ capturePrivateLocalConfigRoot().credentialPath,
85
+ };
80
86
  const { endpoint, teamId } = fungiMcpTeamEndpoint(rawEndpoint);
81
87
  const rootPath = await realpath(resolve(input.folderPath));
82
88
  if (!(await stat(rootPath)).isDirectory())
@@ -142,7 +148,11 @@ export async function attachPrivateLocalComputer(rawEndpoint, input, options = {
142
148
  * Detach a Computer's attachment on the Hub, then forget it here. A pending
143
149
  * attach is replayed first so the Hub never keeps an attachment Local forgot.
144
150
  */
145
- export async function detachPrivateLocalComputer(rawEndpoint, computer, options = {}) {
151
+ export async function detachPrivateLocalComputer(rawEndpoint, computer, input = {}) {
152
+ const options = {
153
+ ...input,
154
+ credentialPath: input.credentialPath ?? capturePrivateLocalConfigRoot().credentialPath,
155
+ };
146
156
  const { endpoint, teamId } = fungiMcpTeamEndpoint(rawEndpoint);
147
157
  const saved = await readFungiMcpLocalBackends(options);
148
158
  const record = saved.backends.find((candidate) => candidate.teamId === teamId && sameComputer(candidate, computer));
@@ -183,7 +193,11 @@ export async function listPrivateLocalComputers(options = {}) {
183
193
  * Serve one attached folder until `signal` aborts or the Hub refuses the
184
194
  * machine (it was detached elsewhere), in which case Local forgets it.
185
195
  */
186
- export async function runPrivateLocalComputer(rawEndpoint, computer, options) {
196
+ export async function runPrivateLocalComputer(rawEndpoint, computer, input) {
197
+ const options = {
198
+ ...input,
199
+ credentialPath: input.credentialPath ?? capturePrivateLocalConfigRoot().credentialPath,
200
+ };
187
201
  const { endpoint, teamId } = fungiMcpTeamEndpoint(rawEndpoint);
188
202
  const saved = await readFungiMcpLocalBackends(options);
189
203
  const record = saved.backends.find((candidate) => candidate.teamId === teamId && sameComputer(candidate, computer));
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import type { LocalSandboxLaunch } from "./private-invocation-computer.js";
2
3
  declare const processSchema: z.ZodObject<{
3
4
  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>>;
4
5
  command: z.ZodString;
@@ -99,7 +100,7 @@ export type PrivateLocalProcessCommandSpec = Readonly<{
99
100
  }>;
100
101
  /** One Session's (or invocation's) view of the host's background processes. */
101
102
  export type PrivateLocalProcessScope = Readonly<{
102
- start: (spec: PrivateLocalProcessCommandSpec, cwd: string, executionPath: string) => Promise<PrivateLocalProcess>;
103
+ start: (spec: PrivateLocalProcessCommandSpec, cwd: string, executionPath: string, sandbox?: LocalSandboxLaunch) => Promise<PrivateLocalProcess>;
103
104
  status: (id: string) => PrivateLocalProcess;
104
105
  list: () => readonly PrivateLocalProcess[];
105
106
  read: (id: string, offset: number, maxBytes: number) => Promise<z.output<typeof readOutput>>;
@@ -111,11 +112,13 @@ export type PrivateLocalComputerProcesses = Readonly<{
111
112
  scope: (key: string) => PrivateLocalProcessScope;
112
113
  /** Stop every process of one scope and remove its logs (Session ended). */
113
114
  disposeScope: (key: string) => Promise<void>;
115
+ /** Whether any background process of any scope is still running. */
116
+ running: () => boolean;
114
117
  /** Stop every process, remove every log, and refuse new starts. */
115
118
  close: () => Promise<void>;
116
119
  }>;
117
120
  /** Open the background-process owner for one invocation root. */
118
- export declare function createPrivateLocalComputerProcesses(root: string): PrivateLocalComputerProcesses;
121
+ export declare function createPrivateLocalComputerProcesses(root: string, home?: string): PrivateLocalComputerProcesses;
119
122
  /**
120
123
  * Background operations share the Computer's one shell/exec surface; a
121
124
  * plain (not discriminated) union, since `start`'s shell and direct branches
@@ -195,5 +198,5 @@ export declare const backgroundExecOutput: z.ZodUnion<readonly [z.ZodObject<{
195
198
  }, z.core.$strict>>;
196
199
  }, z.core.$strict>]>;
197
200
  /** Reuse Session ownership and logs; a wait deadline never cancels the command. */
198
- export declare function callBackgroundExec(scope: PrivateLocalProcessScope, request: z.output<typeof backgroundExecInput>, resolveCwd: (requested: string | undefined) => Promise<string>, executionPath: string, signal: AbortSignal): Promise<z.output<typeof backgroundExecOutput>>;
201
+ export declare function callBackgroundExec(scope: PrivateLocalProcessScope, request: z.output<typeof backgroundExecInput>, resolveCwd: (requested: string | undefined) => Promise<string>, executionPath: string, signal: AbortSignal, sandbox?: LocalSandboxLaunch): Promise<z.output<typeof backgroundExecOutput>>;
199
202
  export {};
@@ -1,13 +1,15 @@
1
- import { spawn } from "node:child_process";
1
+ import { spawn, } from "node:child_process";
2
2
  import { closeSync, openSync, readSync, writeSync } from "node:fs";
3
3
  import { mkdir, rm } from "node:fs/promises";
4
- import { homedir } from "node:os";
5
4
  import { join } from "node:path";
6
5
  import process from "node:process";
7
6
  import { setTimeout as delay } from "node:timers/promises";
8
7
  import { defineIdFamily } from "@fungi.computer/cairn";
9
8
  import { z } from "zod";
10
9
  import { combinedArgvBytes, launchCwdSchema, launchStdinSchema, maximumLaunchTextLength, } from "./private-local-exec-launch.js";
10
+ import { privateLocalExecEnvironment } from "./private-local-exec-environment.js";
11
+ import { capturePrivateLocalConfigRoot } from "./private-local-config-root.js";
12
+ import { Writable } from "node:stream";
11
13
  /**
12
14
  * Background processes of the local Computer: the one owner of "a command that
13
15
  * outlives a single exec call".
@@ -214,7 +216,7 @@ class ManagedProcess {
214
216
  #endedAt = null;
215
217
  #leaderExited = false;
216
218
  command;
217
- constructor(spec, cwd, logDirectory, executionPath) {
219
+ constructor(spec, cwd, logDirectory, executionPath, home, launch) {
218
220
  this.spec = spec;
219
221
  this.cwd = cwd;
220
222
  this.command = describeCommandSpec(spec);
@@ -222,32 +224,49 @@ class ManagedProcess {
222
224
  this.#fd = openSync(this.logPath, "wx+", 0o600);
223
225
  const closed = Promise.withResolvers();
224
226
  this.closed = closed.promise;
225
- const env = { PATH: executionPath, HOME: process.env.HOME ?? homedir() };
227
+ const env = privateLocalExecEnvironment(executionPath, {
228
+ HOME: home,
229
+ XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR,
230
+ DBUS_SESSION_BUS_ADDRESS: process.env.DBUS_SESSION_BUS_ADDRESS,
231
+ });
232
+ if (launch !== undefined) {
233
+ delete env.XDG_RUNTIME_DIR;
234
+ delete env.DBUS_SESSION_BUS_ADDRESS;
235
+ Object.assign(env, launch.environment);
236
+ }
226
237
  try {
227
238
  // Its own process group, so neither exec's finalizer nor this host's
228
239
  // terminal signals reach it; kill() owns ending the whole group.
229
- this.#child =
230
- spec.kind === "shell"
231
- ? spawn("sh", ["-c", spec.command], {
232
- cwd,
233
- env,
234
- detached: true,
235
- stdio: [
236
- spec.stdin === undefined ? "ignore" : "pipe",
237
- "pipe",
238
- "pipe",
239
- ],
240
- })
241
- : spawn(spec.executable, spec.args, {
242
- cwd,
243
- env,
244
- detached: true,
245
- stdio: [
246
- spec.stdin === undefined ? "ignore" : "pipe",
247
- "pipe",
248
- "pipe",
249
- ],
250
- });
240
+ const argv = spec.kind === "shell"
241
+ ? ["sh", "-c", spec.command]
242
+ : [spec.executable, ...spec.args];
243
+ const stdio = [
244
+ spec.stdin === undefined ? "ignore" : "pipe",
245
+ "pipe",
246
+ "pipe",
247
+ ];
248
+ if (launch?.seccompFilter !== undefined)
249
+ stdio.push("pipe");
250
+ this.#child = spawn(launch?.executable ?? argv[0], launch?.args ?? argv.slice(1), {
251
+ cwd,
252
+ env,
253
+ detached: true,
254
+ stdio,
255
+ });
256
+ if (launch?.seccompFilter !== undefined) {
257
+ const filter = this.#child.stdio[3];
258
+ if (!(filter instanceof Writable)) {
259
+ this.#child.kill("SIGKILL");
260
+ throw new Error("Local sandbox filter input fd3 is unavailable");
261
+ }
262
+ // A failed filter delivery cannot become a bare retry or an unhandled
263
+ // stream error. The existing process owner records and reaps failure.
264
+ filter.on("error", (error) => {
265
+ this.#append(Buffer.from(`\n[fungi: sandbox filter failed: ${error.message}]\n`));
266
+ this.killNow();
267
+ });
268
+ filter.end(launch.seccompFilter);
269
+ }
251
270
  // Finite stdin, delivered once and closed; never an interactive
252
271
  // protocol. `end()` writes the bytes and closes the write side. A
253
272
  // child that exits or closes its own stdin before the payload drains
@@ -414,7 +433,7 @@ class ManagedProcess {
414
433
  }
415
434
  }
416
435
  /** Open the background-process owner for one invocation root. */
417
- export function createPrivateLocalComputerProcesses(root) {
436
+ export function createPrivateLocalComputerProcesses(root, home = capturePrivateLocalConfigRoot().home) {
418
437
  const logDirectory = join(root, ".fungi", "processes");
419
438
  const scopes = new Map();
420
439
  let closed = false;
@@ -446,7 +465,7 @@ export function createPrivateLocalComputerProcesses(root) {
446
465
  return record;
447
466
  };
448
467
  return Object.freeze({
449
- start: async (spec, cwd, executionPath) => {
468
+ start: async (spec, cwd, executionPath, sandbox) => {
450
469
  if (closed)
451
470
  throw new Error("Local Computer processes are closed");
452
471
  const records = owned();
@@ -468,7 +487,11 @@ export function createPrivateLocalComputerProcesses(root) {
468
487
  process.on("exit", killAllNow);
469
488
  exitHook = true;
470
489
  }
471
- const started = new ManagedProcess(spec, cwd, logDirectory, executionPath);
490
+ const argv = spec.kind === "shell"
491
+ ? ["sh", "-c", spec.command]
492
+ : [spec.executable, ...spec.args];
493
+ const launch = sandbox?.(cwd, argv);
494
+ const started = new ManagedProcess(spec, cwd, logDirectory, executionPath, home, launch);
472
495
  records.set(started.id, started);
473
496
  return started.snapshot();
474
497
  },
@@ -509,6 +532,7 @@ export function createPrivateLocalComputerProcesses(root) {
509
532
  if (owned !== undefined)
510
533
  await dispose(owned);
511
534
  },
535
+ running: () => all().some((record) => record.running),
512
536
  close: async () => {
513
537
  closed = true;
514
538
  const owned = [...scopes.values()];
@@ -552,7 +576,7 @@ const backgroundResult = readOutput.extend({
552
576
  });
553
577
  export const backgroundExecOutput = z.union([backgroundResult, listOutput]);
554
578
  /** Reuse Session ownership and logs; a wait deadline never cancels the command. */
555
- export async function callBackgroundExec(scope, request, resolveCwd, executionPath, signal) {
579
+ export async function callBackgroundExec(scope, request, resolveCwd, executionPath, signal, sandbox) {
556
580
  signal.throwIfAborted();
557
581
  if (request.operation === "list")
558
582
  return { processes: [...scope.list()] };
@@ -563,7 +587,7 @@ export async function callBackgroundExec(scope, request, resolveCwd, executionPa
563
587
  case "start": {
564
588
  const cwd = await resolveCwd(request.cwd);
565
589
  signal.throwIfAborted();
566
- id = (await scope.start(toCommandSpec(request), cwd, executionPath)).id;
590
+ id = (await scope.start(toCommandSpec(request), cwd, executionPath, sandbox)).id;
567
591
  break;
568
592
  }
569
593
  case "wait":
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The host's selected user home, not an Agent Home or an invocation Files jail.
3
+ * Pass these values through the existing home/path parameters. Capture once at
4
+ * the outer host/client boundary, before awaiting or constructing lazy owners;
5
+ * later opens, reloads and exec launches must not select ambient HOME again.
6
+ *
7
+ * PATH remains the existing per-client/per-Session choice. Neither this value
8
+ * nor an exec child inherits the rest of the host environment.
9
+ */
10
+ export type PrivateLocalConfigRoot = Readonly<{
11
+ home: string;
12
+ credentialPath: string;
13
+ serviceDirectory: string;
14
+ }>;
15
+ /**
16
+ * Select paths without reading credentials, creating directories or starting a
17
+ * service. Keep this capture independent of service/discovery owners, which
18
+ * consume its paths. Explicit path overrides remain the caller's responsibility;
19
+ * there is no settings registry, fallback chain or alternate storage format.
20
+ */
21
+ export declare function capturePrivateLocalConfigRoot(home?: string): PrivateLocalConfigRoot;
@@ -0,0 +1,19 @@
1
+ import { homedir } from "node:os";
2
+ import { isAbsolute, join, resolve } from "node:path";
3
+ import { resolveDefaultCredentialPath } from "./private-file-credential-store.js";
4
+ /**
5
+ * Select paths without reading credentials, creating directories or starting a
6
+ * service. Keep this capture independent of service/discovery owners, which
7
+ * consume its paths. Explicit path overrides remain the caller's responsibility;
8
+ * there is no settings registry, fallback chain or alternate storage format.
9
+ */
10
+ export function capturePrivateLocalConfigRoot(home = homedir()) {
11
+ if (!isAbsolute(home))
12
+ throw new Error("Local configuration home must be absolute");
13
+ const selected = resolve(home);
14
+ return Object.freeze({
15
+ home: selected,
16
+ credentialPath: resolveDefaultCredentialPath(selected),
17
+ serviceDirectory: join(selected, ".fungi", "service"),
18
+ });
19
+ }
@@ -0,0 +1,51 @@
1
+ import { z } from "zod";
2
+ export declare const sandboxUnavailableReasonSchema: z.ZodObject<{
3
+ 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";
11
+ }>;
12
+ message: z.ZodString;
13
+ }, z.core.$strict>;
14
+ export type SandboxUnavailableReason = z.output<typeof sandboxUnavailableReasonSchema>;
15
+ export declare class LocalSandboxUnavailable extends Error {
16
+ readonly reason: SandboxUnavailableReason;
17
+ readonly name = "LocalSandboxUnavailable";
18
+ constructor(reason: SandboxUnavailableReason);
19
+ }
20
+ export type SandboxCommand = Readonly<{
21
+ executable: string;
22
+ args: readonly string[];
23
+ environment: Readonly<{
24
+ TMPDIR: string;
25
+ }>;
26
+ /** Linux only: feed these verified bytes to fd3; never inherit arbitrary fds. */
27
+ seccompFilter?: Uint8Array;
28
+ }>;
29
+ /** Existing foreground/background owners retain process and cancellation lifetime. */
30
+ export type LocalSandbox = Readonly<{
31
+ command(cwd: string, argv: readonly [string, ...string[]]): SandboxCommand;
32
+ close(): void;
33
+ }>;
34
+ export type SandboxAdmission = Readonly<{
35
+ state: "ready";
36
+ sandbox: LocalSandbox;
37
+ }> | Readonly<{
38
+ state: "unavailable";
39
+ reason: SandboxUnavailableReason;
40
+ }>;
41
+ /**
42
+ * The caller already owns the person's auto-approve choice. This module has no
43
+ * mode, setting, bare-command branch, PATH fallback or permission to widen it.
44
+ * A ready result proves only that the backend can start, not the escape matrix.
45
+ */
46
+ export declare function establishLocalSandbox(input: Readonly<{
47
+ project: string;
48
+ /** Absolute folder selected by the Local owner. */
49
+ folder: string;
50
+ platform?: string;
51
+ }>): SandboxAdmission;
@@ -0,0 +1,161 @@
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";
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";
9
+ import { z } from "zod";
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
+ ]),
20
+ message: z.string(),
21
+ });
22
+ export class LocalSandboxUnavailable extends Error {
23
+ reason;
24
+ name = "LocalSandboxUnavailable";
25
+ constructor(reason) {
26
+ super(`Local sandbox unavailable: ${reason.message}`);
27
+ this.reason = reason;
28
+ }
29
+ }
30
+ function unavailable(cause) {
31
+ if (cause instanceof LocalBwrapUnavailable)
32
+ return { kind: cause.reason, message: cause.message };
33
+ return {
34
+ kind: "backend-refused",
35
+ message: cause instanceof Error ? cause.message : "Sandbox preparation failed.",
36
+ };
37
+ }
38
+ /**
39
+ * The caller already owns the person's auto-approve choice. This module has no
40
+ * mode, setting, bare-command branch, PATH fallback or permission to widen it.
41
+ * A ready result proves only that the backend can start, not the escape matrix.
42
+ */
43
+ export function establishLocalSandbox(input) {
44
+ try {
45
+ sandboxPolicy(input.project, input.folder);
46
+ }
47
+ catch (cause) {
48
+ return {
49
+ state: "unavailable",
50
+ reason: {
51
+ kind: "invalid-folder",
52
+ message: cause instanceof Error ? cause.message : "Invalid sandbox folder.",
53
+ },
54
+ };
55
+ }
56
+ const platform = input.platform ?? process.platform;
57
+ if (platform !== "linux" && platform !== "darwin")
58
+ return {
59
+ state: "unavailable",
60
+ reason: {
61
+ kind: "unsupported-platform",
62
+ message: `Local sandbox is unavailable on ${platform}.`,
63
+ },
64
+ };
65
+ let temporaryDirectory;
66
+ 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;
70
+ let closed = false;
71
+ const sandbox = {
72
+ command: (cwd, argv) => {
73
+ if (closed)
74
+ throw new LocalSandboxUnavailable({
75
+ kind: "backend-refused",
76
+ message: "Sandbox has been closed.",
77
+ });
78
+ // Failed/incomplete authority discovery is not an unavailable backend:
79
+ // it must not turn into an authorized unsandboxed retry.
80
+ const policy = sandboxPolicy(input.project, input.folder);
81
+ 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 },
88
+ };
89
+ }
90
+ catch (cause) {
91
+ throw new LocalSandboxUnavailable(unavailable(cause));
92
+ }
93
+ },
94
+ close: () => {
95
+ if (closed)
96
+ return;
97
+ closed = true;
98
+ rmSync(temporary, { recursive: true });
99
+ },
100
+ };
101
+ const command = sandbox.command(realpathSync(input.folder), [
102
+ "/usr/bin/true",
103
+ ]);
104
+ let filterDescriptor;
105
+ const probe = () => {
106
+ if (command.seccompFilter !== undefined) {
107
+ const path = join(temporary, "seccomp.bpf");
108
+ writeFileSync(path, command.seccompFilter, { mode: 0o600 });
109
+ filterDescriptor = openSync(path, "r");
110
+ }
111
+ return spawnSync(command.executable, [...command.args], {
112
+ cwd: input.folder,
113
+ env: { ...command.environment },
114
+ encoding: "utf8",
115
+ timeout: 5_000,
116
+ // Node's maintained spawn allowlist closes every other inherited fd.
117
+ // bwrap loads and closes fd3 before exec (seccomp_program_new).
118
+ stdio: [
119
+ "ignore",
120
+ "pipe",
121
+ "pipe",
122
+ ...(filterDescriptor === undefined ? [] : [filterDescriptor]),
123
+ ],
124
+ });
125
+ };
126
+ const result = (() => {
127
+ try {
128
+ return probe();
129
+ }
130
+ finally {
131
+ if (filterDescriptor !== undefined)
132
+ closeSync(filterDescriptor);
133
+ }
134
+ })();
135
+ if (result.status !== 0) {
136
+ sandbox.close();
137
+ const stderr = result.stderr.trim();
138
+ return {
139
+ state: "unavailable",
140
+ reason: {
141
+ kind: "backend-refused",
142
+ message: result.error?.message ??
143
+ (stderr.length > 0
144
+ ? stderr
145
+ : `Sandbox startup exited ${String(result.status)} (${String(result.signal)}).`),
146
+ },
147
+ };
148
+ }
149
+ return { state: "ready", sandbox };
150
+ }
151
+ catch (cause) {
152
+ if (temporaryDirectory !== undefined)
153
+ rmSync(temporaryDirectory, { recursive: true });
154
+ return {
155
+ state: "unavailable",
156
+ reason: cause instanceof LocalSandboxUnavailable
157
+ ? cause.reason
158
+ : unavailable(cause),
159
+ };
160
+ }
161
+ }