@fungi.computer/truffle 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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-extensions.d.ts +2 -2
- package/dist/local-extensions.js +43 -19
- 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-computer-processes.d.ts +7 -2
- package/dist/private-local-computer-processes.js +47 -23
- package/dist/private-local-delegation.js +15 -11
- 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 +2 -2
- 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 +3 -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 +29 -0
- package/dist/private-local-service.d.ts +3 -0
- package/dist/private-local-service.js +2 -0
- package/package.json +17 -13
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as Effect from "effect/Effect";
|
|
2
2
|
import { AgentArchiveRefused } from "@fungi.computer/shiitake/agent-control";
|
|
3
|
-
import { agentDelegationRequestKey, createFleetDelegation, delegationProvenance, delegationReplyMeta, delegationSendMeta, } from "@fungi.computer/shiitake/fleet";
|
|
3
|
+
import { agentDelegationRequestKey, createFleetDelegation, delegationProvenance, delegationReplyMeta, delegationSendMeta, isOneWayAncestorNotice, } from "@fungi.computer/shiitake/fleet";
|
|
4
4
|
/** How long one Local delegation grant stays valid while it is admitted. */
|
|
5
5
|
const grantLifetimeMs = 60_000;
|
|
6
6
|
/**
|
|
@@ -113,19 +113,22 @@ export function createPrivateLocalDelegation(input) {
|
|
|
113
113
|
sessionId: sourceSessionId,
|
|
114
114
|
});
|
|
115
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 };
|
|
116
125
|
return fleet.start({
|
|
117
126
|
agents: agents.map(({ agentId }) => agentId),
|
|
118
127
|
actions: ["delegate"],
|
|
119
128
|
expiresAt: Date.now() + grantLifetimeMs,
|
|
120
129
|
provenance,
|
|
121
130
|
meta,
|
|
122
|
-
},
|
|
123
|
-
agentId: task.agentId,
|
|
124
|
-
sessionId: task.sessionId,
|
|
125
|
-
delivery: task.delivery,
|
|
126
|
-
text: task.text,
|
|
127
|
-
requestId,
|
|
128
|
-
});
|
|
131
|
+
}, delegation);
|
|
129
132
|
}
|
|
130
133
|
/** Resolve only this sender's receipt, then let the target own the pending guard. */
|
|
131
134
|
async function recall(task, sourceSessionId, signal) {
|
|
@@ -157,9 +160,8 @@ export function createPrivateLocalDelegation(input) {
|
|
|
157
160
|
}
|
|
158
161
|
}
|
|
159
162
|
/**
|
|
160
|
-
* Admit one
|
|
161
|
-
*
|
|
162
|
-
* authenticated reply. Everything else is refused.
|
|
163
|
+
* Admit delegated work, one-way ancestor notices and authenticated replies.
|
|
164
|
+
* Other reply-less mail is refused.
|
|
163
165
|
*/
|
|
164
166
|
async function receive(mail) {
|
|
165
167
|
const mesh = await input.meshAdmission?.(mail.to.agentId);
|
|
@@ -187,6 +189,8 @@ export function createPrivateLocalDelegation(input) {
|
|
|
187
189
|
throw new Error("Local delegated work must reply to its sender");
|
|
188
190
|
return input.accept(mail, mail.meta);
|
|
189
191
|
}
|
|
192
|
+
if (isOneWayAncestorNotice(mail))
|
|
193
|
+
return input.accept(mail, mail.meta);
|
|
190
194
|
if (mail.reply?.outcome === "accepted") {
|
|
191
195
|
const receipt = fleet.recordAcceptedNotice(mail);
|
|
192
196
|
if (receipt === undefined)
|
|
@@ -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;
|
|
@@ -1348,7 +1348,7 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
|
|
|
1348
1348
|
throw new Error("Local escape approval could not be presented");
|
|
1349
1349
|
return pending;
|
|
1350
1350
|
};
|
|
1351
|
-
const policyForSession = (sessionId) => async ({ request, cwd, mode, executionPath }) => {
|
|
1351
|
+
const policyForSession = (sessionId) => async ({ request, cwd, mode, executionPath, context }) => {
|
|
1352
1352
|
if (mode !== "workspace" || !("executable" in request))
|
|
1353
1353
|
return undefined;
|
|
1354
1354
|
let configuredRemotes;
|
|
@@ -1365,7 +1365,7 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
|
|
|
1365
1365
|
executable: request.executable,
|
|
1366
1366
|
ruleId: matched.ruleId,
|
|
1367
1367
|
});
|
|
1368
|
-
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));
|
|
1369
1369
|
};
|
|
1370
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);
|
|
1371
1371
|
processes = localTools.processes;
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import type { ShiitakeAgent } from "@fungi.computer/shiitake";
|
|
4
|
+
/** Private service ingress, never a Team/member or machine-link request. */
|
|
5
|
+
export declare const localScheduleListInputSchema: z.ZodObject<{
|
|
6
|
+
after: z.ZodOptional<z.ZodObject<{
|
|
7
|
+
agentId: z.ZodString;
|
|
8
|
+
kind: z.ZodUnion<readonly [z.ZodLiteral<"almanac">, z.ZodLiteral<"loop">]>;
|
|
9
|
+
after: z.ZodOptional<z.ZodString>;
|
|
10
|
+
}, z.core.$strict>>;
|
|
11
|
+
limit: z.ZodDefault<z.ZodNumber>;
|
|
12
|
+
invocationRoot: z.ZodString;
|
|
13
|
+
}, z.core.$strict>;
|
|
14
|
+
/** Private person-only mutation, distinct from the finite list request. */
|
|
15
|
+
export declare const localScheduleControlInputSchema: z.ZodObject<{
|
|
16
|
+
invocationRoot: z.ZodString;
|
|
17
|
+
target: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
18
|
+
kind: z.ZodLiteral<"almanac">;
|
|
19
|
+
agentId: z.ZodString;
|
|
20
|
+
scheduleId: z.ZodString & z.ZodType<string, string, z.core.$ZodTypeInternals<string, string>>;
|
|
21
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
22
|
+
kind: z.ZodLiteral<"loop">;
|
|
23
|
+
agentId: z.ZodString;
|
|
24
|
+
sessionId: z.ZodString & z.ZodType<string, string, z.core.$ZodTypeInternals<string, string>>;
|
|
25
|
+
loopId: z.ZodString & z.ZodType<string, string, z.core.$ZodTypeInternals<string, string>>;
|
|
26
|
+
}, z.core.$strict>], "kind">;
|
|
27
|
+
}, z.core.$strict>;
|
|
28
|
+
/** Local directory pages reuse the schedule owner's decoder and retained state. */
|
|
29
|
+
export declare const localSchedulePageSchema: Schema.Struct<{
|
|
30
|
+
readonly items: Schema.$Array<Schema.Union<readonly [Schema.Struct<{
|
|
31
|
+
readonly agentId: Schema.String;
|
|
32
|
+
readonly kind: Schema.Literal<"almanac">;
|
|
33
|
+
readonly schedule: Schema.Struct<{
|
|
34
|
+
readonly id: Schema.String;
|
|
35
|
+
readonly sessionId: Schema.String;
|
|
36
|
+
readonly prompt: Schema.String;
|
|
37
|
+
readonly cron: Schema.NullOr<Schema.Struct<{
|
|
38
|
+
readonly expression: Schema.String;
|
|
39
|
+
readonly timeZone: Schema.String;
|
|
40
|
+
}>>;
|
|
41
|
+
readonly nextDueAt: Schema.Int;
|
|
42
|
+
readonly state: Schema.Literals<readonly ["active", "paused", "cancelled", "finished"]>;
|
|
43
|
+
readonly lastCommandId: Schema.NullOr<Schema.String>;
|
|
44
|
+
readonly lastError: Schema.NullOr<Schema.String>;
|
|
45
|
+
readonly attempt: Schema.Int;
|
|
46
|
+
}>;
|
|
47
|
+
}>, Schema.Struct<{
|
|
48
|
+
readonly agentId: Schema.String;
|
|
49
|
+
readonly kind: Schema.Literal<"loop">;
|
|
50
|
+
readonly schedule: Schema.Struct<{
|
|
51
|
+
readonly sessionId: Schema.String;
|
|
52
|
+
readonly loopId: Schema.String;
|
|
53
|
+
readonly origin: Schema.Literals<readonly ["human", "model"]>;
|
|
54
|
+
readonly expiresAt: Schema.NullOr<Schema.Int>;
|
|
55
|
+
readonly prompt: Schema.String;
|
|
56
|
+
readonly mode: Schema.Literals<readonly ["self_paced", "fixed_interval"]>;
|
|
57
|
+
readonly cron: Schema.NullOr<Schema.Struct<{
|
|
58
|
+
readonly expression: Schema.String;
|
|
59
|
+
readonly timeZone: Schema.String;
|
|
60
|
+
}>>;
|
|
61
|
+
readonly state: Schema.Literals<readonly ["active", "paused", "finished", "failed", "cancelled"]>;
|
|
62
|
+
readonly commandId: Schema.String;
|
|
63
|
+
readonly reason: Schema.NullOr<Schema.String>;
|
|
64
|
+
readonly nextDueAt: Schema.NullOr<Schema.Int>;
|
|
65
|
+
readonly lastError: Schema.NullOr<Schema.String>;
|
|
66
|
+
readonly consecutiveFailures: Schema.Int;
|
|
67
|
+
}>;
|
|
68
|
+
}>]>>;
|
|
69
|
+
readonly unavailable: Schema.$Array<Schema.Struct<{
|
|
70
|
+
readonly agentId: Schema.String;
|
|
71
|
+
readonly reason: Schema.Literal<"owner_unavailable">;
|
|
72
|
+
}>>;
|
|
73
|
+
readonly next: Schema.NullOr<Schema.Struct<{
|
|
74
|
+
readonly agentId: Schema.String;
|
|
75
|
+
readonly kind: Schema.Literals<readonly ["almanac", "loop"]>;
|
|
76
|
+
readonly after: Schema.optionalKey<Schema.String>;
|
|
77
|
+
}>>;
|
|
78
|
+
}>;
|
|
79
|
+
export type LocalSchedulePage = typeof localSchedulePageSchema.Type;
|
|
80
|
+
/** One bounded read, borrowing existing admitted Agent owners one at a time. */
|
|
81
|
+
export declare function readLocalSchedulePage(agentIds: readonly string[], open: (agentId: string, signal: AbortSignal) => Promise<{
|
|
82
|
+
owner: Pick<ShiitakeAgent, "extensions">;
|
|
83
|
+
release: () => void;
|
|
84
|
+
}>, input: z.output<typeof localScheduleListInputSchema>, signal: AbortSignal): Promise<LocalSchedulePage>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { agentScheduleListInputSchema, agentSchedulePageSchema, agentScheduleTargetSchema, readAgentSchedulePage, } from "@fungi.computer/shiitake-extensions/schedules";
|
|
4
|
+
import { logEvent } from "./log-event.js";
|
|
5
|
+
/** Private service ingress, never a Team/member or machine-link request. */
|
|
6
|
+
export const localScheduleListInputSchema = z.strictObject({
|
|
7
|
+
invocationRoot: z.string().min(1),
|
|
8
|
+
...agentScheduleListInputSchema.shape,
|
|
9
|
+
});
|
|
10
|
+
/** Private person-only mutation, distinct from the finite list request. */
|
|
11
|
+
export const localScheduleControlInputSchema = z.strictObject({
|
|
12
|
+
invocationRoot: z.string().min(1),
|
|
13
|
+
target: agentScheduleTargetSchema,
|
|
14
|
+
});
|
|
15
|
+
/** Local directory pages reuse the schedule owner's decoder and retained state. */
|
|
16
|
+
export const localSchedulePageSchema = Schema.Struct({
|
|
17
|
+
items: agentSchedulePageSchema.fields.items,
|
|
18
|
+
unavailable: Schema.Array(Schema.Struct({
|
|
19
|
+
agentId: Schema.String,
|
|
20
|
+
reason: Schema.Literal("owner_unavailable"),
|
|
21
|
+
})).check(Schema.isMaxLength(8)),
|
|
22
|
+
next: agentSchedulePageSchema.fields.next,
|
|
23
|
+
});
|
|
24
|
+
/** One bounded read, borrowing existing admitted Agent owners one at a time. */
|
|
25
|
+
export async function readLocalSchedulePage(agentIds, open, input, signal) {
|
|
26
|
+
signal.throwIfAborted();
|
|
27
|
+
const candidates = agentIds
|
|
28
|
+
.filter((agentId) => input.after === undefined ? true : agentId >= input.after.agentId)
|
|
29
|
+
.sort();
|
|
30
|
+
const items = [];
|
|
31
|
+
const unavailable = [];
|
|
32
|
+
// Bound runtime fan-out even when many retained Agents have no schedules.
|
|
33
|
+
const owners = candidates.slice(0, 8);
|
|
34
|
+
for (const agentId of owners) {
|
|
35
|
+
signal.throwIfAborted();
|
|
36
|
+
try {
|
|
37
|
+
const { owner, release } = await open(agentId, signal);
|
|
38
|
+
try {
|
|
39
|
+
const limit = input.limit - items.length;
|
|
40
|
+
const pageInput = input.after?.agentId === agentId
|
|
41
|
+
? { limit, after: input.after }
|
|
42
|
+
: { limit };
|
|
43
|
+
const page = await readAgentSchedulePage(agentId, owner, pageInput);
|
|
44
|
+
signal.throwIfAborted();
|
|
45
|
+
items.push(...page.items);
|
|
46
|
+
if (page.next !== null)
|
|
47
|
+
return { items, unavailable, next: page.next };
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
release();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch (cause) {
|
|
54
|
+
signal.throwIfAborted();
|
|
55
|
+
logEvent("schedule-directory.owner-unavailable", cause);
|
|
56
|
+
unavailable.push({ agentId, reason: "owner_unavailable" });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const unread = candidates.at(owners.length);
|
|
60
|
+
return {
|
|
61
|
+
items,
|
|
62
|
+
unavailable,
|
|
63
|
+
next: unread === undefined ? null : { agentId: unread, kind: "almanac" },
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -10,6 +10,7 @@ import type { ServiceDiagnosticsStatus } from "./private-local-service-log.js";
|
|
|
10
10
|
import type { LocalSandboxStatus } from "./private-local-sandbox.js";
|
|
11
11
|
import type { LocalPermissionMode } from "./private-local-permission-mode.js";
|
|
12
12
|
import type { LocalUiAttachmentLifetime } from "./private-local-ui-attachment.js";
|
|
13
|
+
import type { PrivateLocalScheduleCapability } from "./private-local-schedule-client.js";
|
|
13
14
|
/** Live health, not the on-disk identity record or a cached connection snapshot. */
|
|
14
15
|
export type PrivateLocalServiceStatus = Readonly<{
|
|
15
16
|
/** The client-service wire contract this service speaks. */
|
|
@@ -140,6 +141,8 @@ export type PrivateLocalSessionDiscovery = Readonly<{
|
|
|
140
141
|
}>;
|
|
141
142
|
/** Local process management stays outside the shared Session HTTP contract. */
|
|
142
143
|
export type PrivateLocalServiceManager = Readonly<{
|
|
144
|
+
/** Explicit private person-only capability; never provided by a mesh/App grant. */
|
|
145
|
+
schedules: (invocationRoot: string) => PrivateLocalScheduleCapability;
|
|
143
146
|
reload: (invocationRoot: string) => Promise<string>;
|
|
144
147
|
/** Throws `LocalServiceUpdated` when a live service speaks another protocol. */
|
|
145
148
|
status: () => Promise<PrivateLocalServiceStatus | undefined>;
|
|
@@ -4,7 +4,7 @@ export { sandboxStatusSchema } from "./private-local-sandbox.js";
|
|
|
4
4
|
* The client-service wire contract. Bump it with any change an older client
|
|
5
5
|
* cannot read; a running client then asks its window to restart.
|
|
6
6
|
*/
|
|
7
|
-
export declare const privateLocalServiceProtocol =
|
|
7
|
+
export declare const privateLocalServiceProtocol = 5;
|
|
8
8
|
/** This build's identity: the packed CLI's content hash, or `source` for a source run. */
|
|
9
9
|
export declare function privateLocalServiceVersion(): string;
|
|
10
10
|
/**
|
|
@@ -19,7 +19,7 @@ declare const serviceLifecycleSchema: z.ZodObject<{
|
|
|
19
19
|
startedAt: z.ZodString;
|
|
20
20
|
}, z.core.$loose>;
|
|
21
21
|
declare const serviceRecordSchema: z.ZodObject<{
|
|
22
|
-
protocol: z.ZodLiteral<
|
|
22
|
+
protocol: z.ZodLiteral<5>;
|
|
23
23
|
baseUrl: z.ZodURL;
|
|
24
24
|
version: z.ZodString;
|
|
25
25
|
serviceId: z.ZodString & z.ZodType<import("@fungi.computer/cairn").CairnId<"local-service", "service", 1>, string, z.core.$ZodTypeInternals<import("@fungi.computer/cairn").CairnId<"local-service", "service", 1>, string>>;
|
|
@@ -29,7 +29,7 @@ declare const serviceRecordSchema: z.ZodObject<{
|
|
|
29
29
|
export type ServiceRecord = z.infer<typeof serviceRecordSchema>;
|
|
30
30
|
/** Mandatory live diagnostics; never written into the registration pointer. */
|
|
31
31
|
export declare const liveServiceStatusSchema: z.ZodObject<{
|
|
32
|
-
protocol: z.ZodLiteral<
|
|
32
|
+
protocol: z.ZodLiteral<5>;
|
|
33
33
|
baseUrl: z.ZodURL;
|
|
34
34
|
version: z.ZodString;
|
|
35
35
|
serviceId: z.ZodString & z.ZodType<import("@fungi.computer/cairn").CairnId<"local-service", "service", 1>, string, z.core.$ZodTypeInternals<import("@fungi.computer/cairn").CairnId<"local-service", "service", 1>, string>>;
|
|
@@ -17,7 +17,7 @@ export { sandboxStatusSchema } from "./private-local-sandbox.js";
|
|
|
17
17
|
* The client-service wire contract. Bump it with any change an older client
|
|
18
18
|
* cannot read; a running client then asks its window to restart.
|
|
19
19
|
*/
|
|
20
|
-
export const privateLocalServiceProtocol =
|
|
20
|
+
export const privateLocalServiceProtocol = 5;
|
|
21
21
|
/** This build's identity: the packed CLI's content hash, or `source` for a source run. */
|
|
22
22
|
export function privateLocalServiceVersion() {
|
|
23
23
|
return (z
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { localFailureMetadata, logEvent } from "./log-event.js";
|
|
2
2
|
import { bodyLimit } from "hono/body-limit";
|
|
3
|
+
import { ShiitakeError } from "@fungi.computer/shiitake";
|
|
4
|
+
import { toShiitakeErrorWire } from "@fungi.computer/shiitake/http";
|
|
5
|
+
import { localScheduleControlInputSchema, localScheduleListInputSchema, } from "./private-local-schedules.js";
|
|
3
6
|
import { enrollPrivateLocalMesh, localMeshJoinSchema, } from "./private-local-mesh-enrollment.js";
|
|
4
7
|
import { createLocalMachineEnrollmentAccount, registerFungiAccountOwner, signOutOwnedFungiAccount, } from "./private-fungi-account.js";
|
|
5
8
|
import { createServiceLogLifecycle, } from "./private-local-service-log.js";
|
|
@@ -103,6 +106,9 @@ readRecoveryManifest, root = capturePrivateLocalConfigRoot()) {
|
|
|
103
106
|
const app = new Hono();
|
|
104
107
|
app.onError((error, context) => {
|
|
105
108
|
diagnostics.failure(localFailureMetadata("service.request", error));
|
|
109
|
+
if (error instanceof ShiitakeError &&
|
|
110
|
+
context.req.path.startsWith(`${prefix}/schedules/`))
|
|
111
|
+
return context.json(toShiitakeErrorWire(error), error.cause === "conflict" ? 409 : 400);
|
|
106
112
|
if (error instanceof LocalStoreSchemaError)
|
|
107
113
|
return context.json({
|
|
108
114
|
code: "store_schema_incompatible",
|
|
@@ -308,6 +314,9 @@ readRecoveryManifest, root = capturePrivateLocalConfigRoot()) {
|
|
|
308
314
|
archived: input.archived,
|
|
309
315
|
}));
|
|
310
316
|
});
|
|
317
|
+
app.post(`${prefix}/schedules/list`, bodyLimit({ maxSize: 32 * 1024 }), async (context) => context.json(await owners.schedules(localScheduleListInputSchema.parse(await context.req.json()), context.req.raw.signal)));
|
|
318
|
+
app.post(`${prefix}/schedules/pause`, bodyLimit({ maxSize: 32 * 1024 }), async (context) => context.json(await owners.pauseSchedule(localScheduleControlInputSchema.parse(await context.req.json()), context.req.raw.signal)));
|
|
319
|
+
app.post(`${prefix}/schedules/delete`, bodyLimit({ maxSize: 32 * 1024 }), async (context) => context.json(await owners.deleteSchedule(localScheduleControlInputSchema.parse(await context.req.json()), context.req.raw.signal)));
|
|
311
320
|
app.post(`${prefix}/reload`, async (context) => {
|
|
312
321
|
try {
|
|
313
322
|
const input = z
|