@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.
- package/README.md +27 -4
- package/dist/internal/private-workerd-sandbox-worker.mjs +243 -273
- package/dist/internal/sandbox-metafile.json +35 -30
- package/dist/local-board-agents.d.ts +46 -0
- package/dist/local-board-agents.js +138 -0
- package/dist/local-board-shipping.d.ts +129 -0
- package/dist/local-board-shipping.js +325 -0
- package/dist/local-extensions.d.ts +4 -2
- package/dist/local-extensions.js +56 -22
- package/dist/local-login.js +33 -25
- package/dist/private-fungi-mcp-connection.js +6 -1
- package/dist/private-invocation-computer.d.ts +6 -2
- package/dist/private-invocation-computer.js +20 -13
- package/dist/private-local-agent-control.d.ts +5 -1
- package/dist/private-local-agent-control.js +11 -1
- package/dist/private-local-auth-rpc.d.ts +5 -0
- package/dist/private-local-auth-rpc.js +46 -4
- package/dist/private-local-client-version.js +3 -1
- package/dist/private-local-computer-processes.d.ts +7 -2
- package/dist/private-local-computer-processes.js +47 -23
- package/dist/private-local-delegation.d.ts +18 -0
- package/dist/private-local-delegation.js +84 -26
- package/dist/private-local-exec-environment.d.ts +5 -2
- package/dist/private-local-exec-environment.js +35 -1
- package/dist/private-local-exec-review.js +14 -7
- package/dist/private-local-exec-rules.d.ts +7 -0
- package/dist/private-local-exec-rules.js +23 -6
- package/dist/private-local-extension-records.d.ts +1 -1
- package/dist/private-local-runtime-host.js +54 -27
- package/dist/private-local-sandbox-policy.js +4 -0
- package/dist/private-local-sandbox.d.ts +3 -1
- package/dist/private-local-sandbox.js +2 -2
- package/dist/private-local-schedule-client.d.ts +12 -0
- package/dist/private-local-schedule-client.js +41 -0
- package/dist/private-local-schedules.d.ts +84 -0
- package/dist/private-local-schedules.js +65 -0
- package/dist/private-local-service-contract.d.ts +4 -0
- package/dist/private-local-service-discovery.d.ts +3 -3
- package/dist/private-local-service-discovery.js +1 -1
- package/dist/private-local-service-listener.js +9 -0
- package/dist/private-local-service-owner.d.ts +130 -0
- package/dist/private-local-service-owner.js +90 -2
- package/dist/private-local-service.d.ts +3 -0
- package/dist/private-local-service.js +9 -1
- package/dist/private-local-session-discovery.d.ts +10 -4
- package/dist/private-local-session-discovery.js +37 -4
- package/dist/private-provider-rejection.d.ts +1 -1
- package/dist/private-provider-rejection.js +28 -8
- package/dist/private-sqlite-owner.d.ts +7 -1
- package/dist/private-sqlite-owner.js +14 -2
- 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(
|
|
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("
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
:
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
return
|
|
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
|
-
|
|
304
|
-
this.#append(chunk);
|
|
305
|
-
};
|
|
306
|
-
this.#child.
|
|
307
|
-
|
|
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))
|
|
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
|
};
|