@fungi.computer/truffle 0.5.0 → 0.6.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 (34) hide show
  1. package/README.md +27 -4
  2. package/dist/internal/private-workerd-sandbox-worker.mjs +243 -273
  3. package/dist/internal/sandbox-metafile.json +35 -30
  4. package/dist/local-extensions.d.ts +2 -2
  5. package/dist/local-extensions.js +43 -19
  6. package/dist/private-fungi-mcp-connection.js +6 -1
  7. package/dist/private-invocation-computer.d.ts +6 -2
  8. package/dist/private-invocation-computer.js +20 -13
  9. package/dist/private-local-computer-processes.d.ts +7 -2
  10. package/dist/private-local-computer-processes.js +47 -23
  11. package/dist/private-local-delegation.js +15 -11
  12. package/dist/private-local-exec-environment.d.ts +5 -2
  13. package/dist/private-local-exec-environment.js +35 -1
  14. package/dist/private-local-exec-review.js +14 -7
  15. package/dist/private-local-exec-rules.d.ts +7 -0
  16. package/dist/private-local-exec-rules.js +23 -6
  17. package/dist/private-local-extension-records.d.ts +1 -1
  18. package/dist/private-local-runtime-host.js +2 -2
  19. package/dist/private-local-sandbox-policy.js +4 -0
  20. package/dist/private-local-sandbox.d.ts +3 -1
  21. package/dist/private-local-sandbox.js +2 -2
  22. package/dist/private-local-schedule-client.d.ts +12 -0
  23. package/dist/private-local-schedule-client.js +41 -0
  24. package/dist/private-local-schedules.d.ts +84 -0
  25. package/dist/private-local-schedules.js +65 -0
  26. package/dist/private-local-service-contract.d.ts +3 -0
  27. package/dist/private-local-service-discovery.d.ts +3 -3
  28. package/dist/private-local-service-discovery.js +1 -1
  29. package/dist/private-local-service-listener.js +9 -0
  30. package/dist/private-local-service-owner.d.ts +130 -0
  31. package/dist/private-local-service-owner.js +29 -0
  32. package/dist/private-local-service.d.ts +3 -0
  33. package/dist/private-local-service.js +2 -0
  34. package/package.json +17 -13
@@ -1934,8 +1934,8 @@
1934
1934
  ],
1935
1935
  "format": "esm"
1936
1936
  },
1937
- "../../packages/forage/dist/schemas.js": {
1938
- "bytes": 5755,
1937
+ "../../packages/forage/dist/normalization.js": {
1938
+ "bytes": 3520,
1939
1939
  "imports": [
1940
1940
  {
1941
1941
  "path": "../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/index.js",
@@ -1945,24 +1945,29 @@
1945
1945
  ],
1946
1946
  "format": "esm"
1947
1947
  },
1948
- "../../packages/forage/dist/errors.js": {
1949
- "bytes": 396,
1950
- "imports": [],
1951
- "format": "esm"
1952
- },
1953
- "../../packages/forage/dist/normalization.js": {
1954
- "bytes": 3477,
1948
+ "../../packages/forage/dist/schemas.js": {
1949
+ "bytes": 5698,
1955
1950
  "imports": [
1956
1951
  {
1957
1952
  "path": "../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/index.js",
1958
1953
  "kind": "import-statement",
1959
1954
  "original": "zod"
1955
+ },
1956
+ {
1957
+ "path": "../../packages/forage/dist/normalization.js",
1958
+ "kind": "import-statement",
1959
+ "original": "./normalization.js"
1960
1960
  }
1961
1961
  ],
1962
1962
  "format": "esm"
1963
1963
  },
1964
+ "../../packages/forage/dist/errors.js": {
1965
+ "bytes": 396,
1966
+ "imports": [],
1967
+ "format": "esm"
1968
+ },
1964
1969
  "../../packages/forage/dist/rendering.js": {
1965
- "bytes": 13533,
1970
+ "bytes": 11613,
1966
1971
  "imports": [
1967
1972
  {
1968
1973
  "path": "../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/index.js",
@@ -1978,17 +1983,12 @@
1978
1983
  "path": "../../packages/forage/dist/normalization.js",
1979
1984
  "kind": "import-statement",
1980
1985
  "original": "./normalization.js"
1981
- },
1982
- {
1983
- "path": "../../packages/forage/dist/schemas.js",
1984
- "kind": "import-statement",
1985
- "original": "./schemas.js"
1986
1986
  }
1987
1987
  ],
1988
1988
  "format": "esm"
1989
1989
  },
1990
1990
  "../../packages/forage/dist/describe.js": {
1991
- "bytes": 2934,
1991
+ "bytes": 3157,
1992
1992
  "imports": [
1993
1993
  {
1994
1994
  "path": "../../packages/forage/dist/schemas.js",
@@ -2014,7 +2014,7 @@
2014
2014
  "format": "esm"
2015
2015
  },
2016
2016
  "../../packages/forage/dist/search.js": {
2017
- "bytes": 9616,
2017
+ "bytes": 9656,
2018
2018
  "imports": [
2019
2019
  {
2020
2020
  "path": "../../packages/forage/dist/schemas.js",
@@ -2066,8 +2066,13 @@
2066
2066
  "format": "esm"
2067
2067
  },
2068
2068
  "../../packages/forage/dist/catalog.js": {
2069
- "bytes": 4793,
2069
+ "bytes": 6091,
2070
2070
  "imports": [
2071
+ {
2072
+ "path": "../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/index.js",
2073
+ "kind": "import-statement",
2074
+ "original": "zod"
2075
+ },
2071
2076
  {
2072
2077
  "path": "../../packages/forage/dist/errors.js",
2073
2078
  "kind": "import-statement",
@@ -2107,7 +2112,7 @@
2107
2112
  "format": "esm"
2108
2113
  },
2109
2114
  "../../packages/forage/dist/app-api.js": {
2110
- "bytes": 5198,
2115
+ "bytes": 6114,
2111
2116
  "imports": [
2112
2117
  {
2113
2118
  "path": "../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/index.js",
@@ -2129,7 +2134,7 @@
2129
2134
  "format": "esm"
2130
2135
  },
2131
2136
  "../../packages/forage/dist/index.js": {
2132
- "bytes": 981,
2137
+ "bytes": 996,
2133
2138
  "imports": [
2134
2139
  {
2135
2140
  "path": "../../packages/forage/dist/schemas.js",
@@ -6533,8 +6538,11 @@
6533
6538
  "../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/index.js": {
6534
6539
  "bytesInOutput": 0
6535
6540
  },
6541
+ "../../packages/forage/dist/normalization.js": {
6542
+ "bytesInOutput": 2663
6543
+ },
6536
6544
  "../../packages/forage/dist/schemas.js": {
6537
- "bytesInOutput": 5686
6545
+ "bytesInOutput": 5531
6538
6546
  },
6539
6547
  "../../packages/forage/dist/index.js": {
6540
6548
  "bytesInOutput": 127
@@ -6542,26 +6550,23 @@
6542
6550
  "../../packages/forage/dist/errors.js": {
6543
6551
  "bytesInOutput": 299
6544
6552
  },
6545
- "../../packages/forage/dist/normalization.js": {
6546
- "bytesInOutput": 2924
6547
- },
6548
6553
  "../../packages/forage/dist/rendering.js": {
6549
- "bytesInOutput": 10894
6554
+ "bytesInOutput": 9821
6550
6555
  },
6551
6556
  "../../packages/forage/dist/describe.js": {
6552
- "bytesInOutput": 2413
6557
+ "bytesInOutput": 2570
6553
6558
  },
6554
6559
  "../../packages/forage/dist/search.js": {
6555
- "bytesInOutput": 8128
6560
+ "bytesInOutput": 8262
6556
6561
  },
6557
6562
  "../../packages/forage/dist/list.js": {
6558
6563
  "bytesInOutput": 1793
6559
6564
  },
6560
6565
  "../../packages/forage/dist/catalog.js": {
6561
- "bytesInOutput": 3677
6566
+ "bytesInOutput": 4697
6562
6567
  },
6563
6568
  "../../packages/forage/dist/app-api.js": {
6564
- "bytesInOutput": 1649
6569
+ "bytesInOutput": 1723
6565
6570
  },
6566
6571
  "../../packages/forage/dist/definitions.js": {
6567
6572
  "bytesInOutput": 1100
@@ -6720,7 +6725,7 @@
6720
6725
  "bytesInOutput": 4648
6721
6726
  }
6722
6727
  },
6723
- "bytes": 993376
6728
+ "bytes": 993272
6724
6729
  }
6725
6730
  }
6726
6731
  }
@@ -78,7 +78,7 @@ export declare function localExtensions(options: LocalExtensionsOptions): {
78
78
  * is pinned to the commit Bun's lockfile resolved; updating is another
79
79
  * explicit `add`. Missing declared artifacts are a refusal.
80
80
  */
81
- add(source: string): Promise<LocalInstalledExtension>;
81
+ add(source: string, cwd: string): Promise<LocalInstalledExtension>;
82
82
  /** Remove the package and every activation that references it. */
83
83
  remove(id: string): Promise<boolean>;
84
84
  list(): Promise<readonly LocalExtensionSummary[]>;
@@ -101,7 +101,7 @@ export declare function localExtensions(options: LocalExtensionsOptions): {
101
101
  agentStoreDirectory: string;
102
102
  }>): Promise<{
103
103
  revision: string;
104
- capabilities: ("state" | "attention" | "settled" | "send" | "timers" | "commands" | "projection" | "run_tools" | "files" | "prompt" | "compaction_handoff")[];
104
+ capabilities: ("state" | "prompt" | "attention" | "settled" | "send" | "timers" | "commands" | "projection" | "run_tools" | "files" | "compaction_handoff")[];
105
105
  skills: boolean;
106
106
  }>;
107
107
  /**
@@ -6,6 +6,7 @@ import { Mycelium } from "@fungi.computer/mycelium";
6
6
  import { defineExtension, ExtensionId, } from "@fungi.computer/shiitake/extensions";
7
7
  import * as Predicate from "effect/Predicate";
8
8
  import * as Schema from "effect/Schema";
9
+ import npa from "npm-package-arg";
9
10
  import writeFileAtomic from "write-file-atomic";
10
11
  import { z } from "zod";
11
12
  import { describeExtensionSkills, } from "./local-skill-source.js";
@@ -337,29 +338,52 @@ export function localExtensions(options) {
337
338
  * is pinned to the commit Bun's lockfile resolved; updating is another
338
339
  * explicit `add`. Missing declared artifacts are a refusal.
339
340
  */
340
- async add(source) {
341
+ async add(source, cwd) {
342
+ if (source.length === 0)
343
+ throw new LocalExtensionError("install", "An extension source is required");
344
+ // Bun installs in private staging, not the caller's project. Let the
345
+ // maintained package-spec parser resolve only local sources there first;
346
+ // leave Git and registry specifications to Bun unchanged.
347
+ const parsed = npa(source, cwd);
348
+ const installSource = parsed.type === "directory" || parsed.type === "file"
349
+ ? z.string().parse(parsed.fetchSpec)
350
+ : source;
341
351
  await mkdir(join(root, "staging"), { recursive: true, mode: 0o700 });
342
352
  const staging = await mkdtemp(join(root, "staging", "install-"));
343
353
  try {
354
+ const runBun = async (args, directory) => {
355
+ const child = Bun.spawn([process.execPath, ...args], {
356
+ cwd: directory,
357
+ // A private cache resolves the named ref afresh instead of a
358
+ // cached checkout of an earlier commit.
359
+ env: {
360
+ ...process.env,
361
+ BUN_BE_BUN: "1",
362
+ BUN_INSTALL_CACHE_DIR: join(staging, ".install-cache"),
363
+ },
364
+ stdout: "ignore",
365
+ stderr: "pipe",
366
+ });
367
+ const [status, stderr] = await Promise.all([
368
+ child.exited,
369
+ new Response(child.stderr).text(),
370
+ ]);
371
+ if (status !== 0)
372
+ throw new LocalExtensionError("install", `Could not install ${source}: ${stderr.trim().slice(0, 2_000)}`);
373
+ };
374
+ // Bun links a directory dependency's files back to its source, even
375
+ // with --backend=copyfile. Its ordinary pack operation supplies an
376
+ // isolated package snapshot and the install lock supplies its integrity.
377
+ // Both operations keep lifecycle scripts disabled.
378
+ const archive = join(staging, "source.tgz");
379
+ if (parsed.type === "directory")
380
+ await runBun(["pm", "pack", "--ignore-scripts", "--filename", archive], installSource);
344
381
  await writeFile(join(staging, "package.json"), `${JSON.stringify({ private: true })}\n`);
345
- const child = Bun.spawn([process.execPath, "add", "--ignore-scripts", source], {
346
- cwd: staging,
347
- // A private cache resolves the named ref afresh instead of a
348
- // cached checkout of an earlier commit.
349
- env: {
350
- ...process.env,
351
- BUN_BE_BUN: "1",
352
- BUN_INSTALL_CACHE_DIR: join(staging, ".install-cache"),
353
- },
354
- stdout: "pipe",
355
- stderr: "pipe",
356
- });
357
- const [status, stderr] = await Promise.all([
358
- child.exited,
359
- new Response(child.stderr).text(),
360
- ]);
361
- if (status !== 0)
362
- throw new LocalExtensionError("install", `Could not install ${source}: ${stderr.trim().slice(0, 2_000)}`);
382
+ await runBun([
383
+ "add",
384
+ "--ignore-scripts",
385
+ parsed.type === "directory" ? archive : installSource,
386
+ ], staging);
363
387
  const lock = lockSchema.parse(Bun.JSONC.parse(await readFile(join(staging, "bun.lock"), "utf8")));
364
388
  const dependencies = Object.keys(z
365
389
  .looseObject({ dependencies: z.record(z.string(), z.string()) })
@@ -2,6 +2,7 @@ import { FungiSignInRequiredError } from "./private-fungi-auth-error.js";
2
2
  import { createRequire } from "node:module";
3
3
  import { Client, StreamableHTTPClientTransport, UnauthorizedError, } from "@modelcontextprotocol/client";
4
4
  import { Mycelium } from "@fungi.computer/mycelium";
5
+ import { forageDiscoveryInstructions } from "@fungi.computer/mycelium/effect";
5
6
  import { z } from "zod";
6
7
  import { parseFungiMcpEndpoint } from "./private-fungi-mcp-endpoint.js";
7
8
  import { registerMcpFormElicitation, McpFormApprovalError, } from "./private-mcp-form-elicitation.js";
@@ -83,7 +84,11 @@ export function createPrivateFungiMcpConnection(options) {
83
84
  operations: {
84
85
  execute: Mycelium.operation({
85
86
  replay: "unsafe",
86
- description: "Execute JavaScript on the connected Fungi MCP server. Discover permitted Team Computer operations with await forage.search({query: 'read'}) and await forage.describe({path: 'files.read'}). Explicitly return the result. Returns the original MCP content and isError; it does not run on the local machine.",
87
+ description: [
88
+ "This operation sends its code to the connected Fungi MCP server's execute tool. The following discovery instructions apply inside that remote call, not the outer Local call.",
89
+ forageDiscoveryInstructions,
90
+ "Explicitly return the result. Returns the original MCP content and isError; it does not run on the local machine.",
91
+ ].join(" "),
87
92
  input: z.strictObject({ code: z.string() }),
88
93
  execute: async (input, context) => {
89
94
  try {
@@ -21,6 +21,7 @@ declare const execInput: z.ZodUnion<readonly [z.ZodObject<{
21
21
  justification: z.ZodString;
22
22
  }, z.core.$strict>>;
23
23
  cwd: z.ZodOptional<z.ZodString>;
24
+ env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
24
25
  timeoutMs: z.ZodDefault<z.ZodNumber>;
25
26
  }, z.core.$strict>, z.ZodObject<{
26
27
  executable: z.ZodString;
@@ -31,6 +32,7 @@ declare const execInput: z.ZodUnion<readonly [z.ZodObject<{
31
32
  justification: z.ZodString;
32
33
  }, z.core.$strict>>;
33
34
  cwd: z.ZodOptional<z.ZodString>;
35
+ env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
34
36
  timeoutMs: z.ZodDefault<z.ZodNumber>;
35
37
  }, z.core.$strict>]>;
36
38
  declare const completedOutput: z.ZodObject<{
@@ -247,7 +249,9 @@ export type LocalExecPolicyRun = (input: Parameters<LocalExecEscapeStage>[0]) =>
247
249
  /** Run the recorded command through the normal bounded process door, without a sandbox.
248
250
  * The host supplies the captured PATH and canonical cwd; the record supplies the
249
251
  * exact original request. No model invocation can call this door directly. */
250
- export declare function dispatchApprovedLocalExec(input: unknown, cwd: string, executionPath: string, home: string, processes: PrivateLocalProcessScope, mode: LocalPermissionMode): Promise<ExecOutput | z.output<typeof backgroundExecOutput>>;
252
+ export declare function dispatchApprovedLocalExec(input: unknown, cwd: string, executionPath: string, home: string, processes: PrivateLocalProcessScope, mode: LocalPermissionMode,
253
+ /** Only inline rule matches have a live execute sink; ledger dispatch does not. */
254
+ sink?: InvocationContext["output"]): Promise<ExecOutput | z.output<typeof backgroundExecOutput>>;
251
255
  export type ExecOutput = z.output<typeof execOutput>;
252
256
  export type ProcessOutput = z.output<typeof completedOutput> | z.output<typeof deadlineOutput>;
253
257
  /**
@@ -262,7 +266,7 @@ export declare function runPrivateInvocationProcess(processCommand: ChildProcess
262
266
  requestedMs?: number;
263
267
  /** The real command text, for the timeout message; omitted when not meaningful (e.g. a native helper). */
264
268
  describeCommand?: () => string;
265
- }>): Promise<ProcessOutput>;
269
+ }>, output?: InvocationContext["output"]): Promise<ProcessOutput>;
266
270
  /**
267
271
  * Construct one private invocation-root Computer: bounded shell exec, plus the
268
272
  * background-process operations of `processes` when the host supplies a scope.
@@ -17,7 +17,7 @@ import { z } from "zod";
17
17
  import { backgroundExecInput, backgroundExecOutput, callBackgroundExec, } from "./private-local-computer-processes.js";
18
18
  import { capturePrivateLocalExecPath, parsePrivateLocalExecPath, } from "./private-local-exec-path.js";
19
19
  import { combinedArgvBytes, launchCwdSchema, launchStdinSchema, maximumLaunchTextLength, } from "./private-local-exec-launch.js";
20
- import { privateLocalExecEnvironment } from "./private-local-exec-environment.js";
20
+ import { privateLocalExecEnvironment, privateLocalExecEnvironmentInput, } from "./private-local-exec-environment.js";
21
21
  import { capturePrivateLocalConfigRoot } from "./private-local-config-root.js";
22
22
  const maximumStreamBytes = 64 * 1_024;
23
23
  /** Room inside the stream cap for the visible truncation marker. */
@@ -50,6 +50,7 @@ const runCommonFields = {
50
50
  .strictObject({ justification: z.string().min(1).max(2048) })
51
51
  .optional(),
52
52
  cwd: launchCwdSchema.optional(),
53
+ env: privateLocalExecEnvironmentInput.optional(),
53
54
  // Accepted up to the full invocation deadline and silently clamped to
54
55
  // maximumCommandTimeoutMs (see runCommandAtInvocationBoundary); rejecting
55
56
  // outright here would just trade one confusing failure for another.
@@ -139,10 +140,12 @@ export const shellOutput = z.union([
139
140
  /** Run the recorded command through the normal bounded process door, without a sandbox.
140
141
  * The host supplies the captured PATH and canonical cwd; the record supplies the
141
142
  * exact original request. No model invocation can call this door directly. */
142
- export async function dispatchApprovedLocalExec(input, cwd, executionPath, home, processes, mode) {
143
+ export async function dispatchApprovedLocalExec(input, cwd, executionPath, home, processes, mode,
144
+ /** Only inline rule matches have a live execute sink; ledger dispatch does not. */
145
+ sink) {
143
146
  const request = shellInput.parse(input);
144
147
  if (request.operation === "run") {
145
- const result = await runCommandAtInvocationBoundary(request, cwd, executionPath, home, new AbortController().signal);
148
+ const result = await runCommandAtInvocationBoundary(request, cwd, executionPath, home, new AbortController().signal, undefined, sink);
146
149
  return execOutput.parse({
147
150
  ...result,
148
151
  sandbox: { mode, likelyDenied: false, ranOutsideSandbox: true },
@@ -316,9 +319,13 @@ function nativeGroupFinalizer(process) {
316
319
  })));
317
320
  })));
318
321
  }
319
- function captureBoundedOutput(stream) {
322
+ function captureBoundedOutput(stream, name, output) {
320
323
  const initial = { chunks: [], byteLength: 0, totalBytes: 0 };
321
- return Stream.runFold(stream, () => initial, (captured, chunk) => {
324
+ return Stream.runFold(output === undefined
325
+ ? stream
326
+ : Stream.tap(stream, (bytes) => Effect.sync(() => {
327
+ output.write(name, bytes);
328
+ })), () => initial, (captured, chunk) => {
322
329
  const remaining = keptStreamBytes - captured.byteLength;
323
330
  const totalBytes = captured.totalBytes + chunk.byteLength;
324
331
  if (remaining <= 0)
@@ -406,7 +413,7 @@ function buildRunCommand(request, cwd, executionPath, home, sandbox) {
406
413
  HOME: home,
407
414
  XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR,
408
415
  DBUS_SESSION_BUS_ADDRESS: process.env.DBUS_SESSION_BUS_ADDRESS,
409
- });
416
+ }, request.env);
410
417
  // Effect's own default stdin is an open "pipe" that nothing closes, so an
411
418
  // executable that reads stdin (`cat`, `python3 -`) would otherwise hang
412
419
  // for the full timeout. "ignore" gives it immediate EOF, matching the
@@ -446,14 +453,14 @@ function isMissingExecutableFailure(cause) {
446
453
  (cause.reason._tag === "NotFound" ||
447
454
  cause.reason._tag === "PermissionDenied"));
448
455
  }
449
- async function runCommandAtInvocationBoundary(request, cwd, executionPath, home, signal, sandbox) {
456
+ async function runCommandAtInvocationBoundary(request, cwd, executionPath, home, signal, sandbox, output) {
450
457
  const processCommand = buildRunCommand(request, cwd, executionPath, home, sandbox);
451
458
  const timeoutMs = Math.min(request.timeoutMs, maximumCommandTimeoutMs);
452
459
  try {
453
460
  return await runPrivateInvocationProcess(processCommand, signal, timeoutMs, {
454
461
  requestedMs: request.timeoutMs,
455
462
  describeCommand: () => describeRunRequest(request),
456
- });
463
+ }, output);
457
464
  }
458
465
  catch (cause) {
459
466
  // Spawning the direct form's named executable can fail at the OS level
@@ -475,7 +482,7 @@ async function runCommandAtInvocationBoundary(request, cwd, executionPath, home,
475
482
  }
476
483
  }
477
484
  /** Shared bounded native process owner; callers supply argv/stdin, never shell interpolation. */
478
- export function runPrivateInvocationProcess(processCommand, signal, timeoutMs, describe) {
485
+ export function runPrivateInvocationProcess(processCommand, signal, timeoutMs, describe, output) {
479
486
  return Effect.runPromise(Effect.scoped(Effect.gen(function* () {
480
487
  const startedAt = yield* Clock.currentTimeMillis;
481
488
  const executor = yield* ChildProcessSpawner.ChildProcessSpawner;
@@ -483,8 +490,8 @@ export function runPrivateInvocationProcess(processCommand, signal, timeoutMs, d
483
490
  const process = yield* Effect.acquireRelease(executor.spawn(processCommand), nativeGroupFinalizer).pipe(Effect.provideService(Scope.Scope, processScope));
484
491
  // Keep both readers alive when the exit wait times out. Killing and
485
492
  // reaping the owned group closes the pipes, preserving bounded output.
486
- const stdoutFiber = yield* Effect.forkChild(captureBoundedOutput(process.stdout));
487
- const stderrFiber = yield* Effect.forkChild(captureBoundedOutput(process.stderr));
493
+ const stdoutFiber = yield* Effect.forkChild(captureBoundedOutput(process.stdout, "stdout", output));
494
+ const stderrFiber = yield* Effect.forkChild(captureBoundedOutput(process.stderr, "stderr", output));
488
495
  const exited = yield* timeoutMs === undefined
489
496
  ? Effect.map(process.exitCode, Option.some)
490
497
  : Effect.timeoutOption(process.exitCode, timeoutMs);
@@ -519,7 +526,7 @@ async function execute(root, executionPath, home, request, context, sandbox, mod
519
526
  context.signal.throwIfAborted();
520
527
  const cwd = await resolveCwd(root, request.cwd);
521
528
  context.signal.throwIfAborted();
522
- const result = await runCommandAtInvocationBoundary(request, cwd, executionPath, home, context.signal, sandbox);
529
+ const result = await runCommandAtInvocationBoundary(request, cwd, executionPath, home, context.signal, sandbox, context.output);
523
530
  return execOutput.parse({
524
531
  ...result,
525
532
  sandbox: {
@@ -614,7 +621,7 @@ currentMode = () => "full-access", stageEscape, runPolicy) {
614
621
  return result;
615
622
  }
616
623
  if (processes !== undefined) {
617
- const output = await callBackgroundExec(processes, request, (requested) => resolveCwd(root, requested), path, context.signal, sandbox);
624
+ const output = await callBackgroundExec(processes, request, (requested) => resolveCwd(root, requested), path, context.signal, sandbox, context.output);
618
625
  return jsonValueSchema.parse("text" in output
619
626
  ? {
620
627
  ...output,
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import type { LocalSandboxLaunch } from "./private-invocation-computer.js";
3
+ import type { InvocationContext } from "@fungi.computer/mycelium";
3
4
  declare const processSchema: z.ZodObject<{
4
5
  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>>;
5
6
  command: z.ZodString;
@@ -92,11 +93,13 @@ export type PrivateLocalProcessCommandSpec = Readonly<{
92
93
  readonly kind: "shell";
93
94
  readonly command: string;
94
95
  readonly stdin?: string;
96
+ readonly env?: Readonly<Record<string, string>>;
95
97
  } | {
96
98
  readonly kind: "direct";
97
99
  readonly executable: string;
98
100
  readonly args: readonly string[];
99
101
  readonly stdin?: string;
102
+ readonly env?: Readonly<Record<string, string>>;
100
103
  }>;
101
104
  /** One Session's (or invocation's) view of the host's background processes. */
102
105
  export type PrivateLocalProcessScope = Readonly<{
@@ -104,7 +107,7 @@ export type PrivateLocalProcessScope = Readonly<{
104
107
  status: (id: string) => PrivateLocalProcess;
105
108
  list: () => readonly PrivateLocalProcess[];
106
109
  read: (id: string, offset: number, maxBytes: number) => Promise<z.output<typeof readOutput>>;
107
- wait: (id: string, timeoutMs: number, signal: AbortSignal) => Promise<z.output<typeof waitOutput>>;
110
+ wait: (id: string, timeoutMs: number, signal: AbortSignal, output?: InvocationContext["output"]) => Promise<z.output<typeof waitOutput>>;
108
111
  kill: (id: string) => Promise<z.output<typeof killOutput>>;
109
112
  }>;
110
113
  /** The host's one background-process owner. */
@@ -133,6 +136,7 @@ export declare const backgroundExecInput: z.ZodUnion<readonly [z.ZodObject<{
133
136
  }, z.core.$strict>>;
134
137
  command: z.ZodString;
135
138
  stdin: z.ZodOptional<z.ZodString>;
139
+ env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
136
140
  cwd: z.ZodOptional<z.ZodString>;
137
141
  }, z.core.$strict>, z.ZodObject<{
138
142
  operation: z.ZodLiteral<"start">;
@@ -142,6 +146,7 @@ export declare const backgroundExecInput: z.ZodUnion<readonly [z.ZodObject<{
142
146
  executable: z.ZodString;
143
147
  args: z.ZodDefault<z.ZodArray<z.ZodString>>;
144
148
  stdin: z.ZodOptional<z.ZodString>;
149
+ env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
145
150
  cwd: z.ZodOptional<z.ZodString>;
146
151
  }, z.core.$strict>, z.ZodObject<{
147
152
  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>>;
@@ -213,5 +218,5 @@ export declare const backgroundExecOutput: z.ZodUnion<readonly [z.ZodObject<{
213
218
  }, z.core.$strict>>;
214
219
  }, z.core.$strict>]>;
215
220
  /** Reuse Session ownership and logs; a wait deadline never cancels the command. */
216
- 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>>;
221
+ export declare function callBackgroundExec(scope: PrivateLocalProcessScope, request: z.output<typeof backgroundExecInput>, resolveCwd: (requested: string | undefined) => Promise<string>, executionPath: string, signal: AbortSignal, sandbox?: LocalSandboxLaunch, sink?: InvocationContext["output"]): Promise<z.output<typeof backgroundExecOutput>>;
217
222
  export {};
@@ -7,7 +7,7 @@ import { setTimeout as delay } from "node:timers/promises";
7
7
  import { defineIdFamily } from "@fungi.computer/cairn";
8
8
  import { z } from "zod";
9
9
  import { combinedArgvBytes, launchCwdSchema, launchStdinSchema, maximumLaunchTextLength, } from "./private-local-exec-launch.js";
10
- import { privateLocalExecEnvironment } from "./private-local-exec-environment.js";
10
+ import { privateLocalExecEnvironment, privateLocalExecEnvironmentInput, } from "./private-local-exec-environment.js";
11
11
  import { capturePrivateLocalConfigRoot } from "./private-local-config-root.js";
12
12
  import { Writable } from "node:stream";
13
13
  /**
@@ -70,6 +70,7 @@ const shellStartInput = z.strictObject({
70
70
  .optional(),
71
71
  command: z.string().min(1).max(maximumLaunchTextLength),
72
72
  stdin: launchStdinSchema.optional(),
73
+ env: privateLocalExecEnvironmentInput.optional(),
73
74
  cwd: launchCwdSchema.optional(),
74
75
  });
75
76
  /** The direct form of background `start`: argv run with no shell interpretation. */
@@ -82,6 +83,7 @@ const directStartInput = z
82
83
  executable: z.string().min(1),
83
84
  args: z.array(z.string()).default([]),
84
85
  stdin: launchStdinSchema.optional(),
86
+ env: privateLocalExecEnvironmentInput.optional(),
85
87
  cwd: launchCwdSchema.optional(),
86
88
  })
87
89
  .refine((value) => combinedArgvBytes(value.executable, value.args) <=
@@ -166,18 +168,18 @@ function groupAlive(groupId) {
166
168
  }
167
169
  }
168
170
  function toCommandSpec(request) {
169
- if ("command" in request) {
170
- const spec = { kind: "shell", command: request.command };
171
- return request.stdin === undefined
172
- ? spec
173
- : { ...spec, stdin: request.stdin };
174
- }
175
- const spec = {
176
- kind: "direct",
177
- executable: request.executable,
178
- args: request.args,
179
- };
180
- return request.stdin === undefined ? spec : { ...spec, stdin: request.stdin };
171
+ let spec = "command" in request
172
+ ? { kind: "shell", command: request.command }
173
+ : {
174
+ kind: "direct",
175
+ executable: request.executable,
176
+ args: request.args,
177
+ };
178
+ if (request.stdin !== undefined)
179
+ spec = { ...spec, stdin: request.stdin };
180
+ if (request.env !== undefined)
181
+ spec = { ...spec, env: request.env };
182
+ return spec;
181
183
  }
182
184
  /**
183
185
  * A one-line, human-readable label for a spec; never used for execution.
@@ -221,6 +223,7 @@ class ManagedProcess {
221
223
  #signal = null;
222
224
  #endedAt = null;
223
225
  #leaderExited = false;
226
+ #output = new Map();
224
227
  command;
225
228
  constructor(spec, cwd, logDirectory, executionPath, home, launch) {
226
229
  this.spec = spec;
@@ -234,7 +237,7 @@ class ManagedProcess {
234
237
  HOME: home,
235
238
  XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR,
236
239
  DBUS_SESSION_BUS_ADDRESS: process.env.DBUS_SESSION_BUS_ADDRESS,
237
- });
240
+ }, spec.env);
238
241
  if (launch !== undefined) {
239
242
  delete env.XDG_RUNTIME_DIR;
240
243
  delete env.DBUS_SESSION_BUS_ADDRESS;
@@ -300,11 +303,12 @@ class ManagedProcess {
300
303
  pid !== undefined && Number.isSafeInteger(pid) && pid > 1
301
304
  ? pid
302
305
  : undefined;
303
- const append = (chunk) => {
304
- this.#append(chunk);
305
- };
306
- this.#child.stdout?.on("data", append);
307
- this.#child.stderr?.on("data", append);
306
+ this.#child.stdout?.on("data", (chunk) => {
307
+ this.#append(chunk, "stdout");
308
+ });
309
+ this.#child.stderr?.on("data", (chunk) => {
310
+ this.#append(chunk, "stderr");
311
+ });
308
312
  this.#child.on("error", (error) => {
309
313
  this.#append(Buffer.from(`\n[fungi: process failed: ${error.message}]\n`));
310
314
  this.#finish(closed.resolve);
@@ -327,7 +331,7 @@ class ManagedProcess {
327
331
  get running() {
328
332
  return this.#endedAt === null;
329
333
  }
330
- #append(chunk) {
334
+ #append(chunk, stream = "stderr") {
331
335
  const fd = this.#fd;
332
336
  if (fd === undefined)
333
337
  return;
@@ -342,6 +346,22 @@ class ManagedProcess {
342
346
  writeSync(fd, kept, first, kept.byteLength - first, 0);
343
347
  this.#logBytes += chunk.byteLength;
344
348
  this.#logTruncated = this.#logBytes > maximumLogBytes;
349
+ for (const output of this.#output.keys())
350
+ output.write(stream, chunk);
351
+ }
352
+ /** Observe only new chunks during this wait; no extra pipe reader or replay. */
353
+ observeOutput(output) {
354
+ if (output !== undefined)
355
+ this.#output.set(output, (this.#output.get(output) ?? 0) + 1);
356
+ return () => {
357
+ if (output === undefined)
358
+ return;
359
+ const remaining = (this.#output.get(output) ?? 1) - 1;
360
+ if (remaining === 0)
361
+ this.#output.delete(output);
362
+ else
363
+ this.#output.set(output, remaining);
364
+ };
345
365
  }
346
366
  /** Read atomically with appends, including a wrap in circular storage. */
347
367
  read(offset, maxBytes) {
@@ -504,8 +524,10 @@ export function createPrivateLocalComputerProcesses(root, home = capturePrivateL
504
524
  status: (id) => find(id).snapshot(),
505
525
  list: () => [...(scopes.get(key)?.values() ?? [])].map((r) => r.snapshot()),
506
526
  read: (id, offset, maxBytes) => Promise.resolve(find(id).read(offset, maxBytes)),
507
- wait: async (id, timeoutMs, signal) => {
527
+ wait: async (id, timeoutMs, signal, output) => {
528
+ signal.throwIfAborted();
508
529
  const record = find(id);
530
+ const stopOutput = record.observeOutput(output);
509
531
  const controller = new AbortController();
510
532
  const abandon = () => {
511
533
  controller.abort();
@@ -519,6 +541,7 @@ export function createPrivateLocalComputerProcesses(root, home = capturePrivateL
519
541
  return waitOutput.parse({ process: record.snapshot(), timedOut });
520
542
  }
521
543
  finally {
544
+ stopOutput();
522
545
  signal.removeEventListener("abort", abandon);
523
546
  controller.abort();
524
547
  }
@@ -589,7 +612,7 @@ const backgroundResult = readOutput.extend({
589
612
  });
590
613
  export const backgroundExecOutput = z.union([backgroundResult, listOutput]);
591
614
  /** Reuse Session ownership and logs; a wait deadline never cancels the command. */
592
- export async function callBackgroundExec(scope, request, resolveCwd, executionPath, signal, sandbox) {
615
+ export async function callBackgroundExec(scope, request, resolveCwd, executionPath, signal, sandbox, sink) {
593
616
  signal.throwIfAborted();
594
617
  if (request.operation === "list")
595
618
  return { processes: [...scope.list()] };
@@ -605,7 +628,8 @@ export async function callBackgroundExec(scope, request, resolveCwd, executionPa
605
628
  }
606
629
  case "wait":
607
630
  id = request.id;
608
- timedOut = (await scope.wait(id, request.timeoutMs, signal)).timedOut;
631
+ timedOut = (await scope.wait(id, request.timeoutMs, signal, sink))
632
+ .timedOut;
609
633
  break;
610
634
  case "kill":
611
635
  id = request.id;