@fungi.computer/truffle 0.4.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 (51) 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-board-agents.d.ts +46 -0
  5. package/dist/local-board-agents.js +138 -0
  6. package/dist/local-board-shipping.d.ts +129 -0
  7. package/dist/local-board-shipping.js +325 -0
  8. package/dist/local-extensions.d.ts +4 -2
  9. package/dist/local-extensions.js +56 -22
  10. package/dist/local-login.js +33 -25
  11. package/dist/private-fungi-mcp-connection.js +6 -1
  12. package/dist/private-invocation-computer.d.ts +6 -2
  13. package/dist/private-invocation-computer.js +20 -13
  14. package/dist/private-local-agent-control.d.ts +5 -1
  15. package/dist/private-local-agent-control.js +11 -1
  16. package/dist/private-local-auth-rpc.d.ts +5 -0
  17. package/dist/private-local-auth-rpc.js +46 -4
  18. package/dist/private-local-client-version.js +3 -1
  19. package/dist/private-local-computer-processes.d.ts +7 -2
  20. package/dist/private-local-computer-processes.js +47 -23
  21. package/dist/private-local-delegation.d.ts +18 -0
  22. package/dist/private-local-delegation.js +84 -26
  23. package/dist/private-local-exec-environment.d.ts +5 -2
  24. package/dist/private-local-exec-environment.js +35 -1
  25. package/dist/private-local-exec-review.js +14 -7
  26. package/dist/private-local-exec-rules.d.ts +7 -0
  27. package/dist/private-local-exec-rules.js +23 -6
  28. package/dist/private-local-extension-records.d.ts +1 -1
  29. package/dist/private-local-runtime-host.js +54 -27
  30. package/dist/private-local-sandbox-policy.js +4 -0
  31. package/dist/private-local-sandbox.d.ts +3 -1
  32. package/dist/private-local-sandbox.js +2 -2
  33. package/dist/private-local-schedule-client.d.ts +12 -0
  34. package/dist/private-local-schedule-client.js +41 -0
  35. package/dist/private-local-schedules.d.ts +84 -0
  36. package/dist/private-local-schedules.js +65 -0
  37. package/dist/private-local-service-contract.d.ts +4 -0
  38. package/dist/private-local-service-discovery.d.ts +3 -3
  39. package/dist/private-local-service-discovery.js +1 -1
  40. package/dist/private-local-service-listener.js +9 -0
  41. package/dist/private-local-service-owner.d.ts +130 -0
  42. package/dist/private-local-service-owner.js +90 -2
  43. package/dist/private-local-service.d.ts +3 -0
  44. package/dist/private-local-service.js +9 -1
  45. package/dist/private-local-session-discovery.d.ts +10 -4
  46. package/dist/private-local-session-discovery.js +37 -4
  47. package/dist/private-provider-rejection.d.ts +1 -1
  48. package/dist/private-provider-rejection.js +28 -8
  49. package/dist/private-sqlite-owner.d.ts +7 -1
  50. package/dist/private-sqlite-owner.js +14 -2
  51. package/package.json +29 -16
@@ -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,
@@ -36,11 +36,13 @@ export type LocalArchiveRequest = Readonly<{
36
36
  * creating Agent, so the directory owner can hold the created Agent to what
37
37
  * the creator holds; its archive names who archives.
38
38
  */
39
- export type LocalAgentControlDirectory = Omit<AgentControlDirectory, "create" | "list" | "archive" | "unarchive"> & Readonly<{
39
+ export type LocalAgentControlDirectory = Omit<AgentControlDirectory, "create" | "list" | "archive" | "unarchive" | "answer" | "escalate"> & Readonly<{
40
40
  list: (signal: AbortSignal, options?: Readonly<{
41
41
  archived?: boolean;
42
42
  }>) => Promise<readonly LocalAgentControlAgent[]>;
43
43
  create: (input: LocalCreateRequest, signal: AbortSignal) => Promise<AgentControlAgent>;
44
+ answer: (input: Parameters<NonNullable<AgentControlDirectory["answer"]>>[0], senderAgentId: string, senderSessionId: string, signal: AbortSignal) => ReturnType<NonNullable<AgentControlDirectory["answer"]>>;
45
+ escalate: (input: Parameters<NonNullable<AgentControlDirectory["escalate"]>>[0], senderAgentId: string, senderSessionId: string, signal: AbortSignal) => ReturnType<NonNullable<AgentControlDirectory["escalate"]>>;
44
46
  archive: (input: LocalArchiveRequest, signal: AbortSignal) => Promise<void>;
45
47
  unarchive: (input: LocalArchiveRequest, signal: AbortSignal) => Promise<void>;
46
48
  }>;
@@ -75,6 +77,8 @@ export declare function createPrivateLocalAgentControl(input: Readonly<{
75
77
  }>) => Promise<readonly AgentControlAgent[]>;
76
78
  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>;
77
79
  recall?: (input: import("@fungi.computer/shiitake/agent-control").AgentRecallInput, sourceSessionId: string, signal: AbortSignal) => Promise<import("@fungi.computer/shiitake").CancelResult>;
80
+ answer?: (input: import("@fungi.computer/shiitake/agent-control").AgentControlAnswerInput, senderSessionId: string, signal: AbortSignal) => Promise<import("@fungi.computer/shiitake/agent-control").AgentControlAnswerOutput>;
81
+ escalate?: (input: import("@fungi.computer/shiitake/agent-control").AgentControlEscalateInput, senderSessionId: string, signal: AbortSignal) => Promise<import("@fungi.computer/shiitake/agent-control").AgentControlEscalateOutput>;
78
82
  create?: (input: Readonly<{
79
83
  requestId: string;
80
84
  name?: string;
@@ -8,7 +8,7 @@ import { createConversationAgents } from "@fungi.computer/shiitake-extensions/ro
8
8
  export function createPrivateLocalAgentControl(input) {
9
9
  const conversations = createConversationAgents(input.storage);
10
10
  function bound(parentSessionId) {
11
- const { create, archive, unarchive } = input.directory;
11
+ const { create, archive, unarchive, answer, escalate } = input.directory;
12
12
  const by = { kind: "agent", agentId: input.agentId };
13
13
  return {
14
14
  ...input.directory,
@@ -23,6 +23,16 @@ export function createPrivateLocalAgentControl(input) {
23
23
  createdBySessionId: parentSessionId,
24
24
  }, signal);
25
25
  },
26
+ answer: (request, sourceSessionId, signal) => parentSessionId === undefined
27
+ ? Promise.resolve({ status: "refused", reason: "no_provenance" })
28
+ : sourceSessionId !== parentSessionId
29
+ ? Promise.resolve({ status: "refused", reason: "not_sender" })
30
+ : answer(request, input.agentId, parentSessionId, signal),
31
+ escalate: (request, sourceSessionId, signal) => parentSessionId === undefined
32
+ ? Promise.resolve({ status: "refused", reason: "no_provenance" })
33
+ : sourceSessionId !== parentSessionId
34
+ ? Promise.resolve({ status: "refused", reason: "not_sender" })
35
+ : escalate(request, input.agentId, parentSessionId, signal),
26
36
  };
27
37
  }
28
38
  const control = conversations.control(bound);
@@ -87,8 +87,13 @@ declare const wireResultSchema: z.ZodUnion<readonly [z.ZodDiscriminatedUnion<[z.
87
87
  category: z.ZodLiteral<"cancelled">;
88
88
  }, z.core.$strict>, z.ZodObject<{
89
89
  category: z.ZodLiteral<"no-key-entered">;
90
+ }, z.core.$strict>, z.ZodObject<{
91
+ category: z.ZodLiteral<"prompt-failed">;
92
+ }, z.core.$strict>, z.ZodObject<{
93
+ category: z.ZodLiteral<"missing-authorization-code">;
90
94
  }, z.core.$strict>, z.ZodObject<{
91
95
  category: z.ZodLiteral<"unknown">;
96
+ errorName: z.ZodString;
92
97
  }, z.core.$strict>], "category">;
93
98
  }, z.core.$strict>]>;
94
99
  declare class AuthPromptTarget extends RpcTarget {
@@ -80,6 +80,24 @@ const eventSchema = z.discriminatedUnion("type", [
80
80
  }),
81
81
  ]);
82
82
  const answerSchema = z.string().max(localAuthInputLimit);
83
+ const errorNameSchema = z.string().regex(/^[A-Za-z][A-Za-z0-9_]{0,63}$/u);
84
+ /** A class name only: anything else (or a non-Error) is named generically. */
85
+ function errorNameOf(cause) {
86
+ if (!(cause instanceof Error))
87
+ return "NonError";
88
+ const name = errorNameSchema.safeParse(cause.name);
89
+ return name.success ? name.data : "Error";
90
+ }
91
+ /**
92
+ * The person's own dialog could not show or answer a provider question (for
93
+ * example the prompt failed the renderer's contract). Fixed text, no cause.
94
+ */
95
+ class LocalAuthPromptFailed extends Error {
96
+ constructor() {
97
+ super("Authentication prompt failed");
98
+ this.name = "LocalAuthPromptFailed";
99
+ }
100
+ }
83
101
  /**
84
102
  * The closed set of failures that cross the service. Provider errors can
85
103
  * contain credential material, so no free text, cause or provider message
@@ -98,7 +116,13 @@ const localAuthFailureSchema = z.discriminatedUnion("category", [
98
116
  }),
99
117
  z.strictObject({ category: z.literal("cancelled") }),
100
118
  z.strictObject({ category: z.literal("no-key-entered") }),
101
- z.strictObject({ category: z.literal("unknown") }),
119
+ z.strictObject({ category: z.literal("prompt-failed") }),
120
+ z.strictObject({ category: z.literal("missing-authorization-code") }),
121
+ z.strictObject({
122
+ category: z.literal("unknown"),
123
+ /** The thrown value's class name only, e.g. `TypeError`; never its message. */
124
+ errorName: errorNameSchema,
125
+ }),
102
126
  ]);
103
127
  const wireResultSchema = z.union([
104
128
  authResultSchema,
@@ -110,6 +134,7 @@ const wireResultSchema = z.union([
110
134
  /** Pi 1.3.0's fixed error prefixes; never retain a provider body suffix. */
111
135
  const callbackPortInUse = /^Port \d+ is in use\b/u;
112
136
  const oauthStateMismatch = "OAuth state mismatch";
137
+ const missingAuthorizationCode = "Missing authorization code";
113
138
  const openAITokenRequestFailed = /^OpenAI OAuth token request failed \(([1-5]\d{2})\)/u;
114
139
  function classifyFailure(cause, aborted) {
115
140
  if (aborted)
@@ -133,7 +158,14 @@ function classifyFailure(cause, aborted) {
133
158
  return { category: "callback-port-in-use" };
134
159
  if (cause instanceof Error && cause.message === oauthStateMismatch)
135
160
  return { category: "oauth-state-mismatch" };
136
- return { category: "unknown" };
161
+ if (cause instanceof LocalAuthPromptFailed)
162
+ return { category: "prompt-failed" };
163
+ if (cause instanceof Error && cause.message === missingAuthorizationCode)
164
+ return { category: "missing-authorization-code" };
165
+ return {
166
+ category: "unknown",
167
+ errorName: errorNameOf(cause),
168
+ };
137
169
  }
138
170
  /** Fixed wording per category; the dialog shows exactly this. */
139
171
  function failureText(failure) {
@@ -164,8 +196,12 @@ function failureText(failure) {
164
196
  return "Authentication cancelled";
165
197
  case "no-key-entered":
166
198
  return new LocalAuthRefusal().message;
199
+ case "prompt-failed":
200
+ return "The sign-in dialog could not show the provider's question (prompt-failed). Nothing was saved.";
201
+ case "missing-authorization-code":
202
+ return "No authorization code was found in what was pasted. Paste the code or the full redirect URL.";
167
203
  case "unknown":
168
- return "Local authentication failed";
204
+ return `Local authentication failed (unknown: ${failure.errorName}). Nothing was saved.`;
169
205
  }
170
206
  }
171
207
  /** The client-side error for one typed failure; its message is fixed text. */
@@ -333,7 +369,13 @@ export class PrivateLocalAuthTarget extends RpcTarget {
333
369
  try {
334
370
  if (promptSignal.aborted)
335
371
  cancel();
336
- return answerSchema.parse(await pending.result());
372
+ const answer = await pending.result().then((value) => ({ value }), (cause) => ({ cause }));
373
+ if ("cause" in answer) {
374
+ promptSignal.throwIfAborted();
375
+ // The presenter's error text is not trusted to be secret-free.
376
+ throw new LocalAuthPromptFailed();
377
+ }
378
+ return answerSchema.parse(answer.value);
337
379
  }
338
380
  finally {
339
381
  promptSignal.removeEventListener("abort", cancel);
@@ -5,7 +5,9 @@ import { basename, dirname, join } from "node:path";
5
5
  import { promisify } from "node:util";
6
6
  import { z } from "zod";
7
7
  const execute = promisify(execFile);
8
- const queryOptions = { timeout: 2_000, maxBuffer: 4_096 };
8
+ // A compiled launcher cold-starts a Bun process; 2s sat at twice its quiet-box
9
+ // time and killed the query on a loaded box.
10
+ const queryOptions = { timeout: 10_000, maxBuffer: 4_096 };
9
11
  const buildId = z.string().regex(/^[a-f0-9]{64}$/u);
10
12
  function installedLauncherOnPath() {
11
13
  return new Promise((resolve, reject) => {
@@ -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;
@@ -28,9 +28,27 @@ export declare function createPrivateLocalDelegation(input: Readonly<{
28
28
  name: string;
29
29
  }[] | undefined>) | undefined;
30
30
  accept: (mail: BirdDogMail, meta?: BirdDogMail["meta"], reply?: VerifiedAgentReply) => Promise<Accepted>;
31
+ /**
32
+ * What happens to items a child left waiting on this Agent when the work
33
+ * that was told about them ends. Without it a host leaves them to expire.
34
+ */
35
+ unattended?: Readonly<{
36
+ /** Exact settled facts of one command of this Agent's own Sessions. */
37
+ readSettlement: (sessionId: string, commandId: string) => Promise<CommandSettlement | undefined>;
38
+ /**
39
+ * Pass one child item on to a person, as this Agent's Session that was
40
+ * told about it. A child that already moved on answers that and nothing changes.
41
+ */
42
+ escalate: (item: Readonly<{
43
+ agentId: string;
44
+ sessionId: string;
45
+ itemId: string;
46
+ }>, receiverSessionId: string) => Promise<void>;
47
+ }>;
31
48
  }>): {
32
49
  send: (task: AgentDelegationInput, sourceSessionId: string, signal: AbortSignal, invocation?: AgentDelegationInvocation) => Promise<DelegationReceipt>;
33
50
  recall: (task: AgentRecallInput, sourceSessionId: string, signal: AbortSignal) => Promise<CancelResult>;
34
51
  receive: (mail: BirdDogMail) => Promise<Accepted>;
52
+ close: () => Promise<void>;
35
53
  outstanding: () => Promise<import("@fungi.computer/shiitake/fleet").OutstandingDelegation | undefined>;
36
54
  };