@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
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
1
2
|
import { AgentArchiveRefused } from "@fungi.computer/shiitake/agent-control";
|
|
2
|
-
import { agentDelegationRequestKey, createFleetDelegation, delegationProvenance, delegationReplyMeta, delegationSendMeta, } from "@fungi.computer/shiitake/fleet";
|
|
3
|
+
import { agentDelegationRequestKey, createFleetDelegation, delegationProvenance, delegationReplyMeta, delegationSendMeta, isOneWayAncestorNotice, } from "@fungi.computer/shiitake/fleet";
|
|
3
4
|
/** How long one Local delegation grant stays valid while it is admitted. */
|
|
4
5
|
const grantLifetimeMs = 60_000;
|
|
5
6
|
/**
|
|
@@ -31,7 +32,67 @@ export function createPrivateLocalDelegation(input) {
|
|
|
31
32
|
}
|
|
32
33
|
return agents;
|
|
33
34
|
}
|
|
34
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Items a child left waiting on this Agent, by the reply command that told
|
|
37
|
+
* it. When the work that carried the reply ends, whatever the Agent neither
|
|
38
|
+
* answered nor escalated goes to a person, so no waiting item is silent.
|
|
39
|
+
*/
|
|
40
|
+
const unattended = new Map();
|
|
41
|
+
let poll;
|
|
42
|
+
/** Escalate the items of every reply whose carrying work has settled. */
|
|
43
|
+
async function settleUnattended() {
|
|
44
|
+
const handling = input.unattended;
|
|
45
|
+
if (handling === undefined)
|
|
46
|
+
return;
|
|
47
|
+
for (const [commandId, entry] of unattended) {
|
|
48
|
+
const settlement = await handling
|
|
49
|
+
.readSettlement(entry.sessionId, commandId)
|
|
50
|
+
.catch((cause) => {
|
|
51
|
+
// Not settled as far as anyone can tell; try again on the next poll.
|
|
52
|
+
Effect.runFork(Effect.logWarning("delegation.settlement", cause));
|
|
53
|
+
return undefined;
|
|
54
|
+
});
|
|
55
|
+
if (settlement === undefined)
|
|
56
|
+
continue;
|
|
57
|
+
unattended.delete(commandId);
|
|
58
|
+
for (const itemId of entry.items)
|
|
59
|
+
await handling
|
|
60
|
+
.escalate({ ...entry.child, itemId }, entry.sessionId)
|
|
61
|
+
.catch((cause) => {
|
|
62
|
+
Effect.runFork(Effect.logWarning("delegation.escalate", cause));
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (unattended.size === 0 && poll !== undefined) {
|
|
66
|
+
clearInterval(poll);
|
|
67
|
+
poll = undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function watchUnattended(accepted, receiverSessionId, reply) {
|
|
71
|
+
if (input.unattended === undefined ||
|
|
72
|
+
reply.outcome === "undeliverable" ||
|
|
73
|
+
reply.waitingOnYou === undefined)
|
|
74
|
+
return;
|
|
75
|
+
if (receiverSessionId === undefined)
|
|
76
|
+
return;
|
|
77
|
+
unattended.set(accepted.commandId, {
|
|
78
|
+
sessionId: receiverSessionId,
|
|
79
|
+
child: { agentId: reply.agentId, sessionId: reply.sessionId },
|
|
80
|
+
items: reply.waitingOnYou.map(({ itemId }) => itemId),
|
|
81
|
+
});
|
|
82
|
+
poll ??= setInterval(() => {
|
|
83
|
+
void settleUnattended();
|
|
84
|
+
}, 1_000);
|
|
85
|
+
poll.unref();
|
|
86
|
+
}
|
|
87
|
+
/** Escalate what is owed now and stop watching; the Agent is closing. */
|
|
88
|
+
async function close() {
|
|
89
|
+
await settleUnattended();
|
|
90
|
+
unattended.clear();
|
|
91
|
+
if (poll !== undefined)
|
|
92
|
+
clearInterval(poll);
|
|
93
|
+
poll = undefined;
|
|
94
|
+
}
|
|
95
|
+
/** Send with one caller-owned identity; follow-up and steer replies return here. */
|
|
35
96
|
async function send(task, sourceSessionId, signal, invocation) {
|
|
36
97
|
if (invocation === undefined)
|
|
37
98
|
throw new Error("Delegation requires a tool invocation");
|
|
@@ -43,37 +104,31 @@ export function createPrivateLocalDelegation(input) {
|
|
|
43
104
|
requestId: task.requestId,
|
|
44
105
|
});
|
|
45
106
|
signal.throwIfAborted();
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return await opened.agent.session(task.sessionId).send(task.text, {
|
|
50
|
-
requestId,
|
|
51
|
-
delivery: "steer",
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
finally {
|
|
55
|
-
await opened.release();
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
const meta = delegationSendMeta({ id: input.agentId, name: input.agentId }, invocation.meta);
|
|
107
|
+
// A person reads this name where work waits on its sender.
|
|
108
|
+
const sender = agents.find(({ agentId }) => agentId === input.agentId);
|
|
109
|
+
const meta = delegationSendMeta({ id: input.agentId, name: sender?.name ?? input.agentId }, invocation.meta);
|
|
59
110
|
// The caller's request key owns replay, not the current model program's bytes.
|
|
60
111
|
const provenance = await delegationProvenance({
|
|
61
112
|
agentId: input.agentId,
|
|
62
113
|
sessionId: sourceSessionId,
|
|
63
114
|
});
|
|
64
115
|
signal.throwIfAborted();
|
|
116
|
+
let delegation = {
|
|
117
|
+
agentId: task.agentId,
|
|
118
|
+
sessionId: task.sessionId,
|
|
119
|
+
delivery: task.delivery,
|
|
120
|
+
text: task.text,
|
|
121
|
+
requestId,
|
|
122
|
+
};
|
|
123
|
+
if (task.delivery === "followUp" && task.goal !== undefined)
|
|
124
|
+
delegation = { ...delegation, goal: task.goal };
|
|
65
125
|
return fleet.start({
|
|
66
126
|
agents: agents.map(({ agentId }) => agentId),
|
|
67
127
|
actions: ["delegate"],
|
|
68
128
|
expiresAt: Date.now() + grantLifetimeMs,
|
|
69
129
|
provenance,
|
|
70
130
|
meta,
|
|
71
|
-
},
|
|
72
|
-
agentId: task.agentId,
|
|
73
|
-
sessionId: task.sessionId,
|
|
74
|
-
text: task.text,
|
|
75
|
-
requestId,
|
|
76
|
-
});
|
|
131
|
+
}, delegation);
|
|
77
132
|
}
|
|
78
133
|
/** Resolve only this sender's receipt, then let the target own the pending guard. */
|
|
79
134
|
async function recall(task, sourceSessionId, signal) {
|
|
@@ -105,9 +160,8 @@ export function createPrivateLocalDelegation(input) {
|
|
|
105
160
|
}
|
|
106
161
|
}
|
|
107
162
|
/**
|
|
108
|
-
* Admit one
|
|
109
|
-
*
|
|
110
|
-
* authenticated reply. Everything else is refused.
|
|
163
|
+
* Admit delegated work, one-way ancestor notices and authenticated replies.
|
|
164
|
+
* Other reply-less mail is refused.
|
|
111
165
|
*/
|
|
112
166
|
async function receive(mail) {
|
|
113
167
|
const mesh = await input.meshAdmission?.(mail.to.agentId);
|
|
@@ -135,6 +189,8 @@ export function createPrivateLocalDelegation(input) {
|
|
|
135
189
|
throw new Error("Local delegated work must reply to its sender");
|
|
136
190
|
return input.accept(mail, mail.meta);
|
|
137
191
|
}
|
|
192
|
+
if (isOneWayAncestorNotice(mail))
|
|
193
|
+
return input.accept(mail, mail.meta);
|
|
138
194
|
if (mail.reply?.outcome === "accepted") {
|
|
139
195
|
const receipt = fleet.recordAcceptedNotice(mail);
|
|
140
196
|
if (receipt === undefined)
|
|
@@ -147,7 +203,9 @@ export function createPrivateLocalDelegation(input) {
|
|
|
147
203
|
if (replier === undefined && !authenticatedUndeliverable)
|
|
148
204
|
throw new Error("Local reply sender is outside this project");
|
|
149
205
|
const meta = delegationReplyMeta(delegation, replier?.name ?? delegation.reply.agentId);
|
|
150
|
-
|
|
206
|
+
const accepted = await input.accept({ ...mail, meta }, meta, delegation.reply);
|
|
207
|
+
watchUnattended(accepted, mail.to.sessionId, delegation.reply);
|
|
208
|
+
return accepted;
|
|
151
209
|
}
|
|
152
|
-
return { send, recall, receive, outstanding: fleet.outstanding };
|
|
210
|
+
return { send, recall, receive, close, outstanding: fleet.outstanding };
|
|
153
211
|
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** Shared admission for shell/direct run and managed start. */
|
|
3
|
+
export declare const privateLocalExecEnvironmentInput: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
1
4
|
/**
|
|
2
5
|
* The only host variables a Local exec child receives besides its selected
|
|
3
6
|
* PATH and HOME. Exec runs as the host's own user, so that user's session
|
|
@@ -5,7 +8,7 @@
|
|
|
5
8
|
* `loginctl` and other user-service tools need to find the user manager.
|
|
6
9
|
* Every other host variable, credentials included, stays out.
|
|
7
10
|
*/
|
|
8
|
-
export type PrivateLocalExecEnvironment = {
|
|
11
|
+
export type PrivateLocalExecEnvironment = Record<string, string> & {
|
|
9
12
|
PATH: string;
|
|
10
13
|
HOME: string;
|
|
11
14
|
XDG_RUNTIME_DIR?: string;
|
|
@@ -13,4 +16,4 @@ export type PrivateLocalExecEnvironment = {
|
|
|
13
16
|
};
|
|
14
17
|
export declare function privateLocalExecEnvironment(executionPath: string, host: NodeJS.ProcessEnv & {
|
|
15
18
|
HOME: string;
|
|
16
|
-
}): PrivateLocalExecEnvironment;
|
|
19
|
+
}, extra?: Readonly<Record<string, string>>): PrivateLocalExecEnvironment;
|
|
@@ -1,5 +1,39 @@
|
|
|
1
|
-
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// Extras reach the native sandbox launcher too; its loader controls stay owned
|
|
3
|
+
// by the host rather than loading model-selected code before containment.
|
|
4
|
+
const hostOwnedNames = new Set([
|
|
5
|
+
"PATH",
|
|
6
|
+
"HOME",
|
|
7
|
+
"XDG_RUNTIME_DIR",
|
|
8
|
+
"DBUS_SESSION_BUS_ADDRESS",
|
|
9
|
+
"TMPDIR",
|
|
10
|
+
"SSH_AUTH_SOCK",
|
|
11
|
+
"GIT_DISCOVERY_ACROSS_FILESYSTEM",
|
|
12
|
+
"FUNGI_SANDBOX",
|
|
13
|
+
"FUNGI_SANDBOX_NETWORK_DISABLED",
|
|
14
|
+
"GLIBC_TUNABLES",
|
|
15
|
+
"GCONV_PATH",
|
|
16
|
+
"LOCPATH",
|
|
17
|
+
]);
|
|
18
|
+
const environmentName = z
|
|
19
|
+
.string()
|
|
20
|
+
.regex(/^[A-Z_][A-Z0-9_]*$/u)
|
|
21
|
+
.refine((name) => !hostOwnedNames.has(name) &&
|
|
22
|
+
!name.startsWith("LD_") &&
|
|
23
|
+
!name.startsWith("DYLD_"), "Host-owned environment variable");
|
|
24
|
+
/** Shared admission for shell/direct run and managed start. */
|
|
25
|
+
export const privateLocalExecEnvironmentInput = z
|
|
26
|
+
.record(environmentName, z
|
|
27
|
+
.string()
|
|
28
|
+
.max(1_024)
|
|
29
|
+
.regex(/^[^\0]*$/u, "Environment values cannot contain NUL"))
|
|
30
|
+
.refine((environment) => Object.keys(environment).length <= 64, {
|
|
31
|
+
message: "Exec env accepts at most 64 variables",
|
|
32
|
+
})
|
|
33
|
+
.describe("Up to 64 extra string variables, each at most 1024 UTF-16 code units, with uppercase ASCII names (letters, digits and underscores; no leading digit). Host-owned names, including HOME, PATH, session/sandbox locators and dynamic-loader controls, are refused.");
|
|
34
|
+
export function privateLocalExecEnvironment(executionPath, host, extra = {}) {
|
|
2
35
|
const environment = {
|
|
36
|
+
...extra,
|
|
3
37
|
PATH: executionPath,
|
|
4
38
|
HOME: host.HOME,
|
|
5
39
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import * as Result from "effect/Result";
|
|
2
|
+
import { gitRuleCoverage, hasExtensionInstallRisk, rulePrefix, } from "./private-local-exec-rules.js";
|
|
3
|
+
import { parse, quote } from "shell-quote";
|
|
3
4
|
const gitHookGrantSubcommands = new Set([
|
|
4
5
|
"add",
|
|
5
6
|
"branch",
|
|
@@ -25,6 +26,12 @@ export function localExecEscapeReview(request, cwd, gitGrant) {
|
|
|
25
26
|
? undefined
|
|
26
27
|
: [request.executable, ...(request.args ?? [])];
|
|
27
28
|
const command = request.command ?? quote(argv ?? []);
|
|
29
|
+
// Literal shell tokens only add the extension risk note. They never turn
|
|
30
|
+
// shell text into argv, establish command identity, or permit a saved rule.
|
|
31
|
+
// Opaque shell text keeps its existing once-only generic review.
|
|
32
|
+
const words = argv ??
|
|
33
|
+
Result.getOrElse(Result.try(() => parse(command, {})), () => []);
|
|
34
|
+
const extensionInstall = hasExtensionInstallRisk(words, argv === undefined ? "shell" : "direct");
|
|
28
35
|
const prefix = argv === undefined || (argv[0] === "git" && gitGrant === undefined)
|
|
29
36
|
? undefined
|
|
30
37
|
: rulePrefix(argv, gitGrant?.configuredRemotes);
|
|
@@ -33,9 +40,7 @@ export function localExecEscapeReview(request, cwd, gitGrant) {
|
|
|
33
40
|
cwd,
|
|
34
41
|
justification: request.escape.justification,
|
|
35
42
|
form: argv === undefined ? "shell" : "direct",
|
|
36
|
-
kind:
|
|
37
|
-
? "extension-install"
|
|
38
|
-
: "exec",
|
|
43
|
+
kind: extensionInstall ? "extension-install" : "exec",
|
|
39
44
|
};
|
|
40
45
|
if (argv !== undefined)
|
|
41
46
|
review.argv = argv;
|
|
@@ -44,7 +49,7 @@ export function localExecEscapeReview(request, cwd, gitGrant) {
|
|
|
44
49
|
review.alwaysAllowCoverage =
|
|
45
50
|
prefix[0] === "git"
|
|
46
51
|
? gitRuleCoverage(prefix[1] ?? "")
|
|
47
|
-
: `Direct argv commands beginning ${JSON.stringify(prefix)} in this project; shell commands still ask.`;
|
|
52
|
+
: `Direct argv commands beginning ${JSON.stringify(prefix)} in this project; shell commands${prefix[0] === "fungi" ? " and extension installs" : ""} still ask.`;
|
|
48
53
|
if (prefix[0] === "git") {
|
|
49
54
|
const hookCapable = gitHookGrantSubcommands.has(prefix[1] ?? "");
|
|
50
55
|
const summary = (hookCapable
|
|
@@ -68,7 +73,9 @@ export function localExecEscapeReview(request, cwd, gitGrant) {
|
|
|
68
73
|
review.risk = { summary, detail };
|
|
69
74
|
}
|
|
70
75
|
else {
|
|
71
|
-
const summary =
|
|
76
|
+
const summary = prefix[0] === "fungi"
|
|
77
|
+
? `Matching \`${prefix.join(" ")} …\` commands run outside the sandbox, as you. Extension installs still ask.`
|
|
78
|
+
: `Every \`${prefix.join(" ")} …\` command will run outside the sandbox, as you.`;
|
|
72
79
|
review.risk = { summary, detail: summary };
|
|
73
80
|
}
|
|
74
81
|
}
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import type { ParseEntry } from "shell-quote";
|
|
2
|
+
/**
|
|
3
|
+
* Conservative literal risk, not CLI parsing or executable identity. Global
|
|
4
|
+
* options can occur among positionals. Shell tokens may disclose risk, but
|
|
5
|
+
* only actual direct argv ever reaches the reusable-grant matcher.
|
|
6
|
+
*/
|
|
7
|
+
export declare function hasExtensionInstallRisk(words: readonly ParseEntry[], form: "direct" | "shell"): boolean;
|
|
1
8
|
/** Offer only direct commands that this same matcher can authorize. */
|
|
2
9
|
export declare function rulePrefix(argv: readonly string[], configuredRemotes?: ReadonlySet<string>): string[] | undefined;
|
|
3
10
|
/** Text for the same Git option profile that gates matching, not a second list. */
|
|
@@ -2,6 +2,27 @@ import { lstatSync, mkdirSync, readFileSync, renameSync, rmdirSync, writeFileSyn
|
|
|
2
2
|
import { basename, join } from "node:path";
|
|
3
3
|
import { correlationKey, defineIdFamily } from "@fungi.computer/cairn";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
function fungiWord(word) {
|
|
6
|
+
const parsed = z.string().safeParse(word);
|
|
7
|
+
return parsed.success && basename(parsed.data) === "fungi";
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Conservative literal risk, not CLI parsing or executable identity. Global
|
|
11
|
+
* options can occur among positionals. Shell tokens may disclose risk, but
|
|
12
|
+
* only actual direct argv ever reaches the reusable-grant matcher.
|
|
13
|
+
*/
|
|
14
|
+
export function hasExtensionInstallRisk(words, form) {
|
|
15
|
+
const executable = form === "direct"
|
|
16
|
+
? fungiWord(words[0])
|
|
17
|
+
? 0
|
|
18
|
+
: -1
|
|
19
|
+
: words.findIndex(fungiWord);
|
|
20
|
+
if (executable < 0)
|
|
21
|
+
return false;
|
|
22
|
+
const extension = words.findIndex((word, index) => index > executable && word === "extension");
|
|
23
|
+
return (extension >= 0 &&
|
|
24
|
+
words.some((word, index) => index > extension && word === "add"));
|
|
25
|
+
}
|
|
5
26
|
const prefixSchema = z.array(z.string().min(1).max(4096)).min(1).max(32);
|
|
6
27
|
const projectRuleIds = defineIdFamily({
|
|
7
28
|
family: "local-exec-rules",
|
|
@@ -264,9 +285,7 @@ export function rulePrefix(argv, configuredRemotes) {
|
|
|
264
285
|
? prefix
|
|
265
286
|
: undefined;
|
|
266
287
|
}
|
|
267
|
-
if (
|
|
268
|
-
argv[1] === "extension" &&
|
|
269
|
-
argv[2] === "add")
|
|
288
|
+
if (hasExtensionInstallRisk(argv, "direct"))
|
|
270
289
|
return undefined;
|
|
271
290
|
const parsed = prefixSchema.safeParse(argv.slice(0, 2));
|
|
272
291
|
return parsed.success && validPrefix(parsed.data) ? parsed.data : undefined;
|
|
@@ -416,9 +435,7 @@ export function localExecRules(project) {
|
|
|
416
435
|
};
|
|
417
436
|
const match = (argv, configuredRemotes) => {
|
|
418
437
|
if (unsafeExecutable(argv[0] ?? "") ||
|
|
419
|
-
(
|
|
420
|
-
argv[1] === "extension" &&
|
|
421
|
-
argv[2] === "add"))
|
|
438
|
+
hasExtensionInstallRisk(argv, "direct"))
|
|
422
439
|
return undefined;
|
|
423
440
|
if (argv[0] === "git" && !safeGitCommand(argv, configuredRemotes))
|
|
424
441
|
return undefined;
|
|
@@ -7,6 +7,7 @@ export declare const activationsSchema: z.ZodObject<{
|
|
|
7
7
|
revision: z.ZodString;
|
|
8
8
|
capabilities: z.ZodArray<z.ZodEnum<{
|
|
9
9
|
state: "state";
|
|
10
|
+
prompt: "prompt";
|
|
10
11
|
attention: "attention";
|
|
11
12
|
settled: "settled";
|
|
12
13
|
send: "send";
|
|
@@ -15,7 +16,6 @@ export declare const activationsSchema: z.ZodObject<{
|
|
|
15
16
|
projection: "projection";
|
|
16
17
|
run_tools: "run_tools";
|
|
17
18
|
files: "files";
|
|
18
|
-
prompt: "prompt";
|
|
19
19
|
compaction_handoff: "compaction_handoff";
|
|
20
20
|
}>>;
|
|
21
21
|
skills: z.ZodBoolean;
|
|
@@ -46,7 +46,7 @@ import { createPrivateInvocationFilesPort, } from "./private-invocation-files.js
|
|
|
46
46
|
import { createLocalSkillSource } from "./local-skill-source.js";
|
|
47
47
|
import { createPrivateWorkerdSandbox } from "./private-workerd-sandbox.js";
|
|
48
48
|
import { createPrivateFungiMcpConnection, } from "./private-fungi-mcp-connection.js";
|
|
49
|
-
import { createPrivateSqliteOwner, openPrivateWritableSqliteDatabase, } from "./private-sqlite-owner.js";
|
|
49
|
+
import { createPrivateSqliteOwner, openPrivateWritableSqliteDatabase, storeWriteBusyTimeoutMs, } from "./private-sqlite-owner.js";
|
|
50
50
|
import { createPrivateLocalMailRoom, hasPrivateLocalPendingMail, } from "./private-local-mail.js";
|
|
51
51
|
import { createPrivateLocalDelegation, } from "./private-local-delegation.js";
|
|
52
52
|
import { createPrivateLocalAgentControl, } from "./private-local-agent-control.js";
|
|
@@ -207,6 +207,30 @@ async function assertPrivateLocalReceiptFile(path, kind) {
|
|
|
207
207
|
throw cause;
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
|
+
function ensureReceiptTable(database) {
|
|
211
|
+
database.exec(`
|
|
212
|
+
CREATE TABLE IF NOT EXISTS ${localCreateReceiptTable} (
|
|
213
|
+
request_id TEXT PRIMARY KEY NOT NULL,
|
|
214
|
+
agent_id TEXT NOT NULL UNIQUE,
|
|
215
|
+
name TEXT,
|
|
216
|
+
created_by_agent_id TEXT,
|
|
217
|
+
created_by_session_id TEXT
|
|
218
|
+
) WITHOUT ROWID
|
|
219
|
+
`);
|
|
220
|
+
const receiptColumns = database
|
|
221
|
+
.prepare(`PRAGMA table_info(${localCreateReceiptTable})`)
|
|
222
|
+
.all()
|
|
223
|
+
.map((column) => column.name);
|
|
224
|
+
if (!receiptColumns.includes("created_by_agent_id"))
|
|
225
|
+
database.exec(`ALTER TABLE ${localCreateReceiptTable} ADD COLUMN created_by_agent_id TEXT`);
|
|
226
|
+
if (!receiptColumns.includes("created_by_session_id"))
|
|
227
|
+
database.exec(`ALTER TABLE ${localCreateReceiptTable} ADD COLUMN created_by_session_id TEXT`);
|
|
228
|
+
}
|
|
229
|
+
function readReceiptRow(database, requestId) {
|
|
230
|
+
return database
|
|
231
|
+
.prepare(`SELECT agent_id, created_by_agent_id, created_by_session_id FROM ${localCreateReceiptTable} WHERE request_id = ?`)
|
|
232
|
+
.get(requestId);
|
|
233
|
+
}
|
|
210
234
|
/** Resolve an Agent create request through its durable Local receipt. */
|
|
211
235
|
export async function resolvePrivateLocalCreateStore(root, requestId, creator) {
|
|
212
236
|
if (requestId.length === 0 || requestId.length > 256)
|
|
@@ -229,27 +253,13 @@ export async function resolvePrivateLocalCreateStore(root, requestId, creator) {
|
|
|
229
253
|
try {
|
|
230
254
|
database.exec(`PRAGMA busy_timeout = ${String(receiptBusyTimeoutMs)}`);
|
|
231
255
|
database.exec("BEGIN IMMEDIATE");
|
|
232
|
-
database
|
|
233
|
-
|
|
234
|
-
request_id TEXT PRIMARY KEY NOT NULL,
|
|
235
|
-
agent_id TEXT NOT NULL UNIQUE,
|
|
236
|
-
name TEXT,
|
|
237
|
-
created_by_agent_id TEXT,
|
|
238
|
-
created_by_session_id TEXT
|
|
239
|
-
) WITHOUT ROWID
|
|
240
|
-
`);
|
|
241
|
-
const receiptColumns = database
|
|
242
|
-
.prepare(`PRAGMA table_info(${localCreateReceiptTable})`)
|
|
243
|
-
.all()
|
|
244
|
-
.map((column) => column.name);
|
|
245
|
-
if (!receiptColumns.includes("created_by_agent_id"))
|
|
246
|
-
database.exec(`ALTER TABLE ${localCreateReceiptTable} ADD COLUMN created_by_agent_id TEXT`);
|
|
247
|
-
if (!receiptColumns.includes("created_by_session_id"))
|
|
248
|
-
database.exec(`ALTER TABLE ${localCreateReceiptTable} ADD COLUMN created_by_session_id TEXT`);
|
|
249
|
-
const existing = database
|
|
250
|
-
.prepare(`SELECT agent_id, created_by_agent_id, created_by_session_id FROM ${localCreateReceiptTable} WHERE request_id = ?`)
|
|
251
|
-
.get(requestId);
|
|
256
|
+
ensureReceiptTable(database);
|
|
257
|
+
const existing = readReceiptRow(database, requestId);
|
|
252
258
|
if (existing !== undefined) {
|
|
259
|
+
if (creatorAgentId !== undefined &&
|
|
260
|
+
(existing.created_by_agent_id === null ||
|
|
261
|
+
existing.created_by_agent_id === undefined))
|
|
262
|
+
throw new AgentCreateRefused("request_conflict", "the Local Agent create receipt is unbound; use a fresh requestId");
|
|
253
263
|
if (creatorAgentId !== undefined &&
|
|
254
264
|
(existing.created_by_agent_id !== creatorAgentId ||
|
|
255
265
|
existing.created_by_session_id !== creatorSessionId))
|
|
@@ -616,6 +626,13 @@ export function createPrivateLocalModelSource(models, providerId, modelId, provi
|
|
|
616
626
|
message.stopReason === "error" &&
|
|
617
627
|
modelFailure?.message === message &&
|
|
618
628
|
modelFailure.retryable,
|
|
629
|
+
retryAfterMs: (message) => modelFailure?.message === message &&
|
|
630
|
+
modelFailure.rejection?.code === "rate-limit"
|
|
631
|
+
? modelFailure.rejection.retryAfterMs
|
|
632
|
+
: undefined,
|
|
633
|
+
rejection: (message) => modelFailure?.message === message
|
|
634
|
+
? modelFailure.rejection
|
|
635
|
+
: undefined,
|
|
619
636
|
failureReason: (message) => modelFailure?.message === message ? modelFailure.reason : undefined,
|
|
620
637
|
providerError: (message) => modelFailure?.message === message
|
|
621
638
|
? modelFailure.providerError
|
|
@@ -637,7 +654,7 @@ export function createPrivateLocalModelSource(models, providerId, modelId, provi
|
|
|
637
654
|
const evidence = {
|
|
638
655
|
refusal: turn.refusal(),
|
|
639
656
|
upstream: upstream ?? partial,
|
|
640
|
-
rejection: provider.rejection(),
|
|
657
|
+
rejection: provider.rejection(upstream),
|
|
641
658
|
};
|
|
642
659
|
const message = cause === undefined
|
|
643
660
|
? modelFailureMessage(selected.provider, evidence)
|
|
@@ -674,6 +691,8 @@ export function createPrivateLocalModelSource(models, providerId, modelId, provi
|
|
|
674
691
|
reason: message,
|
|
675
692
|
retryable: modelFailureRetryable(evidence),
|
|
676
693
|
};
|
|
694
|
+
if (evidence.rejection !== undefined)
|
|
695
|
+
modelFailure.rejection = evidence.rejection;
|
|
677
696
|
if (upstream?.errorMessage !== undefined)
|
|
678
697
|
modelFailure.providerError = upstream.errorMessage;
|
|
679
698
|
}
|
|
@@ -1044,7 +1063,7 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
|
|
|
1044
1063
|
providerCacheKey = createHash("sha256")
|
|
1045
1064
|
.update(JSON.stringify(["fungi-local", paths.databasePath, initialSessionId]))
|
|
1046
1065
|
.digest("hex");
|
|
1047
|
-
database = openPrivateWritableSqliteDatabase(paths.databasePath,
|
|
1066
|
+
database = openPrivateWritableSqliteDatabase(paths.databasePath, storeWriteBusyTimeoutMs);
|
|
1048
1067
|
database.exec("PRAGMA foreign_keys = ON");
|
|
1049
1068
|
const agentId = ensureLocalAgentId({
|
|
1050
1069
|
database,
|
|
@@ -1119,6 +1138,14 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
|
|
|
1119
1138
|
return Promise.reject(new Error("Local mail ingress is unavailable"));
|
|
1120
1139
|
return acceptMail(mail, meta, reply);
|
|
1121
1140
|
},
|
|
1141
|
+
unattended: {
|
|
1142
|
+
readSettlement: (sessionId, commandId) => readSettlement === undefined
|
|
1143
|
+
? Promise.reject(new Error("Local settlement reads are unavailable"))
|
|
1144
|
+
: readSettlement(sessionId, commandId),
|
|
1145
|
+
escalate: async (item, receiverSessionId) => {
|
|
1146
|
+
await input.agentControl?.escalate(item, agentId, receiverSessionId, new AbortController().signal);
|
|
1147
|
+
},
|
|
1148
|
+
},
|
|
1122
1149
|
});
|
|
1123
1150
|
let projectDirectory;
|
|
1124
1151
|
if (input.agentControl !== undefined) {
|
|
@@ -1321,7 +1348,7 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
|
|
|
1321
1348
|
throw new Error("Local escape approval could not be presented");
|
|
1322
1349
|
return pending;
|
|
1323
1350
|
};
|
|
1324
|
-
const policyForSession = (sessionId) => async ({ request, cwd, mode, executionPath }) => {
|
|
1351
|
+
const policyForSession = (sessionId) => async ({ request, cwd, mode, executionPath, context }) => {
|
|
1325
1352
|
if (mode !== "workspace" || !("executable" in request))
|
|
1326
1353
|
return undefined;
|
|
1327
1354
|
let configuredRemotes;
|
|
@@ -1338,7 +1365,7 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
|
|
|
1338
1365
|
executable: request.executable,
|
|
1339
1366
|
ruleId: matched.ruleId,
|
|
1340
1367
|
});
|
|
1341
|
-
return shellOutput.parse(await dispatchApprovedLocalExec(request, cwd, executionPath, configRoot.home, localTools.processes.scope(sessionId), mode));
|
|
1368
|
+
return shellOutput.parse(await dispatchApprovedLocalExec(request, cwd, executionPath, configRoot.home, localTools.processes.scope(sessionId), mode, context.output));
|
|
1342
1369
|
};
|
|
1343
1370
|
const localTools = await createPrivateLocalTools(selectedFolder.absolute, input.fungiMcp, subagents?.directory, (sessionId) => sessionPaths.get(sessionId), dirname(paths.databasePath), subagents, await input.configuration?.(dirname(paths.databasePath)), configRoot, sandboxOwner, () => sandboxOwner.status().mode, escapeForSession, policyForSession);
|
|
1344
1371
|
processes = localTools.processes;
|
|
@@ -1531,7 +1558,7 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
|
|
|
1531
1558
|
};
|
|
1532
1559
|
},
|
|
1533
1560
|
close: () => {
|
|
1534
|
-
closing ??= closePrivateLocalResources({
|
|
1561
|
+
closing ??= (delegation?.close() ?? Promise.resolve()).then(() => closePrivateLocalResources({
|
|
1535
1562
|
agent,
|
|
1536
1563
|
processes,
|
|
1537
1564
|
sandbox,
|
|
@@ -1539,7 +1566,7 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
|
|
|
1539
1566
|
database,
|
|
1540
1567
|
lease,
|
|
1541
1568
|
providerCacheKey,
|
|
1542
|
-
});
|
|
1569
|
+
}));
|
|
1543
1570
|
return closing;
|
|
1544
1571
|
},
|
|
1545
1572
|
});
|
|
@@ -90,6 +90,10 @@ osHome, platform = "linux") {
|
|
|
90
90
|
mode,
|
|
91
91
|
temporaryDirectory,
|
|
92
92
|
fungiCache: protectedFungiCache,
|
|
93
|
+
// Adding PATH lookup directories would invalidate the current name-based
|
|
94
|
+
// extension-install risk classification; revisit private-local-exec-rules.ts
|
|
95
|
+
// before widening this set. Guard: ../test/private-local-containment.test.ts,
|
|
96
|
+
// "keeps every captured PATH directory outside Workspace writable roots".
|
|
93
97
|
writable: mode === "workspace"
|
|
94
98
|
? [...new Set([policy.writableRoot, ...writableCaches])]
|
|
95
99
|
: [],
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { NativeSourceReader } from "./private-local-bwrap.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { sandboxUnavailableReasonSchema, type SandboxUnavailableReason } from "./private-local-containment.js";
|
|
3
|
+
import { establishLocalSandbox, sandboxUnavailableReasonSchema, type SandboxUnavailableReason } from "./private-local-containment.js";
|
|
4
4
|
import type { LocalPermissionMode } from "./private-local-permission-mode.js";
|
|
5
5
|
import type { LocalSandboxWrites } from "./private-invocation-files.js";
|
|
6
6
|
import type { NativePayloadStaging } from "./private-local-native-staging.js";
|
|
@@ -48,6 +48,8 @@ export declare function createLocalSandboxControls(input: Readonly<{
|
|
|
48
48
|
platform?: string;
|
|
49
49
|
readNativePatchSource?: NativeSourceReader;
|
|
50
50
|
native?: NativePayloadStaging | undefined;
|
|
51
|
+
/** Test-only hook for inspecting probe launches without native execution. */
|
|
52
|
+
runProbe?: Parameters<typeof establishLocalSandbox>[2];
|
|
51
53
|
}>): LocalSandboxControls;
|
|
52
54
|
/** Probe a fresh Agent before deciding its mode; never treat an unavailable backend as a sandboxed run. */
|
|
53
55
|
export declare function probeLocalSandbox(project: string, folder: string, native?: NativePayloadStaging): SandboxUnavailableReason | undefined;
|
|
@@ -97,7 +97,7 @@ export function createLocalSandboxControls(input) {
|
|
|
97
97
|
temporaryDirectory: temporaryDirectory ??
|
|
98
98
|
agentSandboxTemporaryDirectory(temporaryOwner, input.project),
|
|
99
99
|
...platform,
|
|
100
|
-
}, native);
|
|
100
|
+
}, native, input.runProbe);
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
catch (cause) {
|
|
@@ -167,7 +167,7 @@ export function createLocalSandboxControls(input) {
|
|
|
167
167
|
mode: nextMode,
|
|
168
168
|
temporaryDirectory,
|
|
169
169
|
...platform,
|
|
170
|
-
}, native);
|
|
170
|
+
}, native, input.runProbe);
|
|
171
171
|
if (admission.state === "ready") {
|
|
172
172
|
sandbox = admission.sandbox;
|
|
173
173
|
preparedMode = nextMode;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import { type AgentScheduleRow, type AgentScheduleTarget } from "@fungi.computer/shiitake-extensions/schedules";
|
|
3
|
+
import { localScheduleListInputSchema, type LocalSchedulePage } from "./private-local-schedules.js";
|
|
4
|
+
import type { PrivateLocalServiceRequestOptions, PrivateLocalServiceStatus } from "./private-local-service-contract.js";
|
|
5
|
+
/** A private, folder-bound owner capability; it contains no public URL or grant. */
|
|
6
|
+
export type PrivateLocalScheduleCapability = Readonly<{
|
|
7
|
+
list(input?: Omit<z.input<typeof localScheduleListInputSchema>, "invocationRoot">, options?: PrivateLocalServiceRequestOptions): Promise<LocalSchedulePage>;
|
|
8
|
+
pause(target: AgentScheduleTarget, options?: PrivateLocalServiceRequestOptions): Promise<AgentScheduleRow>;
|
|
9
|
+
delete(target: AgentScheduleTarget, options?: PrivateLocalServiceRequestOptions): Promise<AgentScheduleRow>;
|
|
10
|
+
}>;
|
|
11
|
+
/** Only a native private service client creates this capability for its person. */
|
|
12
|
+
export declare function createPrivateLocalScheduleCapability(invocationRoot: string, start: (options?: PrivateLocalServiceRequestOptions) => Promise<PrivateLocalServiceStatus>): PrivateLocalScheduleCapability;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import { parseShiitakeError } from "@fungi.computer/shiitake/client";
|
|
3
|
+
import { agentScheduleRowSchema, } from "@fungi.computer/shiitake-extensions/schedules";
|
|
4
|
+
import { readBoundedResponse } from "./private-bounded-response.js";
|
|
5
|
+
import { localScheduleControlInputSchema, localScheduleListInputSchema, localSchedulePageSchema, } from "./private-local-schedules.js";
|
|
6
|
+
// A page has at most 64 prompts of 256 KiB, including JSON escaping and metadata.
|
|
7
|
+
// This is a wire budget, not a limit on retained records or future occurrences.
|
|
8
|
+
const maximumResponseBytes = 128 * 1024 * 1024;
|
|
9
|
+
/** Only a native private service client creates this capability for its person. */
|
|
10
|
+
export function createPrivateLocalScheduleCapability(invocationRoot, start) {
|
|
11
|
+
async function call(operation, input, output, options = {}) {
|
|
12
|
+
options.signal?.throwIfAborted();
|
|
13
|
+
const service = await start(options);
|
|
14
|
+
const request = {
|
|
15
|
+
method: "POST",
|
|
16
|
+
headers: { "Content-Type": "application/json" },
|
|
17
|
+
body: JSON.stringify(input),
|
|
18
|
+
redirect: "error",
|
|
19
|
+
};
|
|
20
|
+
if (options.signal !== undefined)
|
|
21
|
+
request.signal = options.signal;
|
|
22
|
+
const response = await fetch(`${service.baseUrl}/service/${service.serviceId}/schedules/${operation}`, request);
|
|
23
|
+
const body = await readBoundedResponse(response, maximumResponseBytes, `Private Local schedule ${operation}`);
|
|
24
|
+
if (!response.ok) {
|
|
25
|
+
let failure;
|
|
26
|
+
try {
|
|
27
|
+
failure = parseShiitakeError(body);
|
|
28
|
+
}
|
|
29
|
+
catch (cause) {
|
|
30
|
+
throw new Error(`Private Local schedule ${operation} failed (${String(response.status)})`, { cause });
|
|
31
|
+
}
|
|
32
|
+
throw failure;
|
|
33
|
+
}
|
|
34
|
+
return Schema.decodeUnknownSync(output, { onExcessProperty: "error" })(JSON.parse(body));
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
list: (input = {}, options) => call("list", localScheduleListInputSchema.parse({ ...input, invocationRoot }), localSchedulePageSchema, options),
|
|
38
|
+
pause: (target, options) => call("pause", localScheduleControlInputSchema.parse({ invocationRoot, target }), agentScheduleRowSchema, options),
|
|
39
|
+
delete: (target, options) => call("delete", localScheduleControlInputSchema.parse({ invocationRoot, target }), agentScheduleRowSchema, options),
|
|
40
|
+
};
|
|
41
|
+
}
|