@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.
@@ -1,7 +1,10 @@
1
1
  import { isAbsolute, relative, resolve, sep } from "node:path";
2
+ import { tmpdir } from "node:os";
3
+ import { fileURLToPath } from "node:url";
4
+ import { createNativePayloadStaging } from "./private-local-native-staging.js";
2
5
  import { z } from "zod";
3
- import { establishLocalSandbox, LocalSandboxUnavailable, sandboxUnavailableReasonSchema, } from "./private-local-containment.js";
4
- import { protectedSandboxPaths, resolveAgentFolder, sandboxPolicy, } from "./private-local-sandbox-policy.js";
6
+ import { establishLocalSandbox, LocalSandboxUnavailable, sandboxUnavailableReason, sandboxUnavailableReasonSchema, } from "./private-local-containment.js";
7
+ import { protectedSandboxPaths, assertHostCodeOutsideAgent, UnsafeHostCode, resolveAgentFolder, sandboxPolicy, } from "./private-local-sandbox-policy.js";
5
8
  /** Availability is not a hostile-program security or an approval status. */
6
9
  export const sandboxStatusSchema = z.discriminatedUnion("state", [
7
10
  z.strictObject({
@@ -25,59 +28,126 @@ function contains(root, path) {
25
28
  export function createLocalSandboxControls(input) {
26
29
  const required = input.required();
27
30
  const folder = resolveAgentFolder(input.project, input.folder);
31
+ // The packed Node wrapper passes the paths it actually loaded. In source
32
+ // runs this module is the executing host authority instead. Check before
33
+ // staging or probing, independent of the optional/required setting.
34
+ try {
35
+ const launcher = process.env.FUNGI_LAUNCHER;
36
+ const cli = launcher === undefined
37
+ ? fileURLToPath(import.meta.url)
38
+ : process.env.FUNGI_BUNDLED_CLI;
39
+ if (cli === undefined)
40
+ throw new UnsafeHostCode("Packed CLI entry was not provided by the launcher; reinstall Fungi outside the project.");
41
+ assertHostCodeOutsideAgent(input.project, cli, launcher ?? cli);
42
+ }
43
+ catch (cause) {
44
+ throw new LocalSandboxUnavailable(cause instanceof UnsafeHostCode
45
+ ? sandboxUnavailableReason(cause)
46
+ : {
47
+ kind: "unsafe-host-code",
48
+ message: `Fungi host JavaScript identity cannot be verified (${cause instanceof Error ? cause.message : String(cause)}); reinstall Fungi using a copying install outside the project.`,
49
+ });
50
+ }
51
+ const platform = input.platform === undefined ? {} : { platform: input.platform };
52
+ const native = createNativePayloadStaging(input.project, input.readNativePatchSource, input.probeNativeRunLiveness);
28
53
  const initial = establishLocalSandbox({
29
54
  project: input.project,
30
55
  folder: folder.absolute,
31
- });
56
+ ...platform,
57
+ }, native);
32
58
  let sandbox = initial.state === "ready" ? initial.sandbox : undefined;
59
+ let everReady = initial.state === "ready";
33
60
  let unavailable = initial.state === "unavailable" ? initial.reason : undefined;
34
61
  if (unavailable !== undefined) {
35
- if (unavailable.kind === "invalid-folder")
62
+ if (unavailable.kind === "invalid-folder") {
63
+ native.close();
36
64
  throw new Error(unavailable.message);
37
- if (required)
65
+ }
66
+ if (required) {
67
+ native.close();
38
68
  throw new LocalSandboxUnavailable(unavailable);
69
+ }
39
70
  }
40
71
  let closed = false;
41
72
  const assertFolder = () => {
42
73
  if (closed)
43
74
  throw new Error("Local sandbox owner is closed");
44
- const current = resolveAgentFolder(input.project, input.folder);
45
- if (current.absolute !== folder.absolute)
75
+ try {
76
+ const current = resolveAgentFolder(input.project, input.folder);
77
+ if (current.absolute === folder.absolute)
78
+ return;
46
79
  throw new Error("Local Agent folder authority changed while open");
80
+ }
81
+ catch (cause) {
82
+ if (!everReady)
83
+ throw cause;
84
+ unavailable = {
85
+ kind: "invalid-folder",
86
+ message: cause instanceof Error ? cause.message : String(cause),
87
+ };
88
+ throw new LocalSandboxUnavailable(unavailable);
89
+ }
47
90
  };
48
- const launch = (cwd, argv) => {
91
+ function launch(cwd, argv, readOnlyHelperFile) {
49
92
  assertFolder();
50
93
  if (sandbox === undefined) {
51
94
  const admission = establishLocalSandbox({
52
95
  project: input.project,
53
96
  folder: folder.absolute,
54
- });
55
- if (admission.state === "ready")
97
+ ...platform,
98
+ }, native);
99
+ if (admission.state === "ready") {
56
100
  sandbox = admission.sandbox;
101
+ everReady = true;
102
+ }
57
103
  else
58
104
  unavailable = admission.reason;
59
105
  }
60
106
  if (sandbox !== undefined) {
61
107
  try {
62
- const command = sandbox.command(cwd, argv);
108
+ const command = sandbox.command(cwd, argv, readOnlyHelperFile);
63
109
  unavailable = undefined;
64
110
  return command;
65
111
  }
66
112
  catch (cause) {
67
- if (!(cause instanceof LocalSandboxUnavailable))
68
- throw cause;
69
- unavailable = cause.reason;
113
+ unavailable =
114
+ cause instanceof LocalSandboxUnavailable
115
+ ? cause.reason
116
+ : sandboxUnavailableReason(cause);
117
+ throw new LocalSandboxUnavailable(unavailable);
70
118
  }
71
119
  }
72
120
  if (unavailable === undefined)
73
121
  throw new Error("Local sandbox preparation produced no result");
74
- if (unavailable.kind === "invalid-folder")
122
+ if (unavailable.kind === "invalid-folder" && !everReady)
75
123
  throw new Error(unavailable.message);
124
+ // A formerly ready sandbox never becomes optional bare execution.
125
+ // Before readiness, only availability failures permit fallback.
126
+ if (everReady ||
127
+ (unavailable.kind !== "missing" &&
128
+ unavailable.kind !== "unsupported-platform" &&
129
+ unavailable.kind !== "startup-refused"))
130
+ throw new LocalSandboxUnavailable(unavailable);
76
131
  if (input.required())
77
132
  throw new LocalSandboxUnavailable(unavailable);
78
- // No executable has started. Only this owner admits the bare fallback.
133
+ // Files is never handed back its installed executable for a bare retry.
134
+ if (readOnlyHelperFile !== undefined) {
135
+ try {
136
+ return {
137
+ bareNative: true,
138
+ executable: native.patch(readOnlyHelperFile),
139
+ args: [],
140
+ environment: { TMPDIR: process.env.TMPDIR ?? tmpdir() },
141
+ };
142
+ }
143
+ catch (cause) {
144
+ unavailable = sandboxUnavailableReason(cause);
145
+ throw new LocalSandboxUnavailable(unavailable);
146
+ }
147
+ }
148
+ // No executable has started. Generic exec retains its ordinary fallback.
79
149
  return undefined;
80
- };
150
+ }
81
151
  const assertReady = () => {
82
152
  launch(folder.absolute, ["/usr/bin/true"]);
83
153
  };
@@ -108,7 +178,12 @@ export function createLocalSandboxControls(input) {
108
178
  if (closed)
109
179
  return;
110
180
  closed = true;
111
- sandbox?.close();
181
+ try {
182
+ sandbox?.close();
183
+ }
184
+ finally {
185
+ native.close();
186
+ }
112
187
  },
113
188
  };
114
189
  }
@@ -131,10 +131,10 @@ export declare const archiveRefusalSchema: z.ZodObject<{
131
131
  reason: z.ZodEnum<{
132
132
  pending: "pending";
133
133
  running: "running";
134
+ unreadable: "unreadable";
134
135
  archived: "archived";
135
136
  not_creator: "not_creator";
136
137
  self: "self";
137
- unreadable: "unreadable";
138
138
  }>;
139
139
  error: z.ZodString;
140
140
  }, z.core.$strict>;
@@ -114,7 +114,7 @@ onRecoveryDisabled?: ServiceLogLifecycle["recoveryDisabled"], configRoot?: Priva
114
114
  state: "unavailable";
115
115
  requireSandbox: boolean;
116
116
  reason: {
117
- kind: "missing" | "invalid-digest" | "digest-mismatch" | "not-executable" | "unsupported-platform" | "invalid-folder" | "backend-refused";
117
+ kind: "missing" | "missing-artifact" | "invalid-digest" | "digest-mismatch" | "unreadable" | "not-executable" | "staging-failed" | "unsupported-platform" | "invalid-folder" | "unsafe-native-payload" | "unsafe-host-code" | "startup-refused" | "backend-refused";
118
118
  message: string;
119
119
  };
120
120
  }>;
@@ -232,12 +232,19 @@ export function createPrivateLocalServiceManager(options = {}) {
232
232
  if (await serviceOwnerIsLive(directory))
233
233
  return "busy";
234
234
  const bundledCli = process.env.FUNGI_BUNDLED_CLI;
235
- const child = spawn(options.executable ?? process.execPath, bundledCli === undefined
236
- ? [
237
- fileURLToPath(new URL("./private-local-service-process.js", import.meta.url)),
238
- directory,
239
- ]
240
- : [bundledCli, "--internal-service", directory], {
235
+ // A detached Bun service also starts in the invoking project's cwd.
236
+ // Keep the host process from importing Agent-authored bunfig or .env.
237
+ const bunFlags = options.executable === undefined && "bun" in process.versions
238
+ ? ["--config=/dev/null", "--no-env-file"]
239
+ : [];
240
+ const serviceEntry = bundledCli ??
241
+ fileURLToPath(new URL("./private-local-service-process.js", import.meta.url));
242
+ const child = spawn(options.executable ?? process.execPath, [
243
+ ...bunFlags,
244
+ serviceEntry,
245
+ ...(bundledCli === undefined ? [] : ["--internal-service"]),
246
+ directory,
247
+ ], {
241
248
  detached: true,
242
249
  stdio: "ignore",
243
250
  // Service startup retains its existing inherited inputs. This is
@@ -37,7 +37,7 @@ export declare function readPrivateLocalAgentProfile(path: string): {
37
37
  model: {
38
38
  readonly providerId: string;
39
39
  readonly modelId: string;
40
- readonly reasoning?: "minimal" | "low" | "medium" | "high" | "xhigh" | "max" | undefined;
40
+ readonly reasoning?: "high" | "low" | "max" | "medium" | "minimal" | "xhigh" | undefined;
41
41
  } | null;
42
42
  };
43
43
  export declare function readPrivateLocalSessionSummaries(path: string): Promise<readonly LocalSummary[]>;
@@ -12,7 +12,7 @@ RUN if [ "$TARGET" = linux-arm64 ]; then \
12
12
  fi && apt-get update && apt-get install -y --no-install-recommends \
13
13
  build-essential ca-certificates curl xz-utils python3-pip ninja-build \
14
14
  pkg-config patchelf libcap-dev libseccomp-dev \
15
- $(if [ "$TARGET" = linux-arm64 ]; then printf 'gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu libcap-dev:arm64'; fi) \
15
+ $(if [ "$TARGET" = linux-arm64 ]; then printf 'gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu libc6-dev-arm64-cross libcap-dev:arm64'; fi) \
16
16
  && rm -rf /var/lib/apt/lists/*
17
17
  # prefer_static is a maintained Meson option newer than Jammy's Meson.
18
18
  RUN pip3 install --no-cache-dir meson==1.10.2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fungi.computer/truffle",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "private": false,
5
5
  "description": "Truffle: Fungi's local Agent runtime, bundled into the fungi command",
6
6
  "type": "module",
@@ -69,18 +69,18 @@
69
69
  "@fungi.computer/carrier": "0.1.0",
70
70
  "@fungi.computer/cordyceps": "0.3.1",
71
71
  "@fungi.computer/gills": "0.2.0",
72
- "@fungi.computer/graft": "0.1.0",
73
- "@fungi.computer/knapsack": "0.2.0",
72
+ "@fungi.computer/graft": "0.1.1",
73
+ "@fungi.computer/knapsack": "0.2.1",
74
74
  "@fungi.computer/lantern": "0.3.0",
75
- "@fungi.computer/mycelium": "0.2.0",
75
+ "@fungi.computer/mycelium": "0.2.1",
76
76
  "@fungi.computer/nacre": "0.0.1",
77
77
  "@fungi.computer/seam": "0.1.0",
78
- "@fungi.computer/shiitake": "0.3.0",
79
- "@fungi.computer/shiitake-extensions": "0.1.0",
78
+ "@fungi.computer/shiitake": "0.3.1",
79
+ "@fungi.computer/shiitake-extensions": "0.1.1",
80
80
  "@fungi.computer/veil": "0.2.0",
81
81
  "@fungi.computer/watchdog": "0.1.0",
82
82
  "@fungi.computer/whistle": "0.1.0",
83
- "@fungi.computer/zookeeper": "0.0.1",
83
+ "@fungi.computer/zookeeper": "0.0.2",
84
84
  "@modelcontextprotocol/client": "2.0.0",
85
85
  "diff": "8.0.4",
86
86
  "effect": "4.0.2",
@@ -97,7 +97,7 @@
97
97
  },
98
98
  "devDependencies": {
99
99
  "@cloudflare/codemode": "0.5.1",
100
- "@fungi.computer/mule": "0.1.0",
100
+ "@fungi.computer/mule": "0.1.1",
101
101
  "@modelcontextprotocol/server": "2.0.0",
102
102
  "@types/node": "24.13.3",
103
103
  "@types/proper-lockfile": "4.1.4",
@@ -140,7 +140,7 @@
140
140
  "scripts": {
141
141
  "lint": "../../node_modules/.bin/eslint src scripts test --max-warnings=0 && ../../node_modules/.bin/oxlint --config ../../oxlint.config.ts --deny-warnings src scripts test",
142
142
  "typecheck": "tsc -p tsconfig.json --noEmit",
143
- "build": "pnpm run clean && tsc -p tsconfig.build.json && node scripts/copy-seatbelt.mjs && esbuild src/internal/private-workerd-sandbox-worker.mjs --outfile=dist/internal/private-workerd-sandbox-worker.mjs --bundle --format=esm --platform=neutral --conditions=workerd,browser --target=es2024 --external:cloudflare:workers --metafile=dist/internal/sandbox-metafile.json --log-level=warning && esbuild src/internal/local-image-fit-worker.mjs --outfile=dist/internal/local-image-fit-worker.mjs --bundle --format=esm --platform=node --target=es2024 --metafile=dist/internal/image-fit-metafile.json --log-level=warning",
143
+ "build": "if [ ! -f src/private-local-native-digests.js ]; then pnpm run build:native; fi && node scripts/generate-native-digests.mjs --verify && pnpm run clean && tsc -p tsconfig.build.json && cp src/private-local-native-digests.js dist/private-local-native-digests.js && node scripts/copy-seatbelt.mjs && esbuild src/internal/private-workerd-sandbox-worker.mjs --outfile=dist/internal/private-workerd-sandbox-worker.mjs --bundle --format=esm --platform=neutral --conditions=workerd,browser --target=es2024 --external:cloudflare:workers --metafile=dist/internal/sandbox-metafile.json --log-level=warning && esbuild src/internal/local-image-fit-worker.mjs --outfile=dist/internal/local-image-fit-worker.mjs --bundle --format=esm --platform=node --target=es2024 --metafile=dist/internal/image-fit-metafile.json --log-level=warning",
144
144
  "clean": "node --input-type=module --eval \"import { rm } from 'node:fs/promises'; await rm('dist', { recursive: true, force: true });\"",
145
145
  "test": "pnpm run test:node && pnpm run test:merge && pnpm run test:stop-all",
146
146
  "test:stop-all": "bun test ./test/private-local-stop-all.bun.test.ts",
@@ -149,7 +149,7 @@
149
149
  "test:bun": "pnpm run test:mesh-link && pnpm run test:bun:batch && bun test ./test/private-workerd-deadline.bun.test.ts",
150
150
  "test:mesh-link": "bun test ./test/private-local-mesh-links.bun.test.ts",
151
151
  "test:bun:batch": "bun test ./test/private-sqlite-approval-driver.bun.test.ts ./test/mcp-form-service.bun.test.ts ./test/private-local-service-discovery.bun.test.ts ./test/private-local-service-listener.bun.test.ts ./test/private-local-service-manager.bun.test.ts ./test/private-local-service-rebind.bun.test.ts ./test/private-local-service-stop.bun.test.ts ./test/private-local-service-ensure.bun.test.ts ./test/private-local-service-identity.bun.test.ts ./test/private-local-service-kill-resume.bun.test.ts ./test/private-local-service-idle-owners.bun.test.ts ./test/private-local-service-idle-delegation.bun.test.ts ./test/private-local-mail-retry.bun.test.ts ./test/private-local-service-log-native.bun.test.ts ./test/private-local-service-log.bun.test.ts ./test/private-local-sqlite-lifetime.bun.test.ts ./test/private-sqlite-driver.bun.test.ts ./test/private-local-reload.bun.test.ts ./test/private-local-auth-rpc.bun.test.ts ./test/private-workerd-forage.bun.test.ts ./test/local-extensions.bun.test.ts ./test/local-zookeeper-slot.bun.test.ts ./test/local-extension-skills.bun.test.ts ./test/private-local-sandbox-service.bun.test.ts",
152
- "build:native": "fungi-apply-patch-build --out native/bin && node scripts/build-bwrap.mjs --out native/bin",
152
+ "build:native": "fungi-apply-patch-build --out native/bin && node scripts/build-bwrap.mjs --out native/bin && node scripts/generate-native-digests.mjs",
153
153
  "test:patch": "node --test --test-timeout=30000 test/apply-patch.native.mjs",
154
154
  "test:sandbox:linux": "node --import tsx test/fixtures/local-sandbox-proof.mts",
155
155
  "test:sandbox:macos": "scratch=$(mktemp -d /tmp/fungi-seatbelt.XXXXXX) && scratch=$(cd \"$scratch\" && pwd -P) && mkdir \"$scratch/home\" && HOME=\"$scratch/home\" node --import tsx scripts/seatbelt-mac-proof.mjs \"$scratch\"",