@fungi.computer/truffle 0.3.1 → 0.5.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/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 +2 -0
- package/dist/local-extensions.js +121 -36
- package/dist/local-login.js +33 -19
- package/dist/local-upgrade.d.ts +63 -0
- package/dist/local-upgrade.js +358 -0
- package/dist/private-local-agent-control.d.ts +8 -1
- package/dist/private-local-agent-control.js +16 -2
- 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-delegation.d.ts +18 -0
- package/dist/private-local-delegation.js +82 -21
- package/dist/private-local-exec-review.js +45 -8
- package/dist/private-local-extension-archive-notice.d.ts +8 -0
- package/dist/private-local-extension-archive-notice.js +108 -0
- package/dist/private-local-extension-records.d.ts +25 -0
- package/dist/private-local-extension-records.js +51 -0
- package/dist/private-local-process-identity.d.ts +14 -0
- package/dist/private-local-process-identity.js +51 -0
- package/dist/private-local-runtime-host.d.ts +7 -1
- package/dist/private-local-runtime-host.js +88 -31
- package/dist/private-local-service-contract.d.ts +1 -0
- package/dist/private-local-service-discovery.js +2 -3
- package/dist/private-local-service-owner.js +80 -9
- package/dist/private-local-service-termination.js +12 -11
- package/dist/private-local-service.js +19 -1
- package/dist/private-local-session-discovery.d.ts +16 -4
- package/dist/private-local-session-discovery.js +68 -4
- package/dist/private-mcp-form-contract.d.ts +13 -0
- package/dist/private-mcp-form-contract.js +6 -1
- package/dist/private-mcp-form-session.d.ts +26 -0
- package/dist/private-mcp-form-session.js +8 -5
- 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 +32 -16
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
1
2
|
import { AgentArchiveRefused } from "@fungi.computer/shiitake/agent-control";
|
|
2
3
|
import { agentDelegationRequestKey, createFleetDelegation, delegationProvenance, delegationReplyMeta, delegationSendMeta, } from "@fungi.computer/shiitake/fleet";
|
|
3
4
|
/** How long one Local delegation grant stays valid while it is admitted. */
|
|
@@ -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,19 +104,9 @@ 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,
|
|
@@ -71,6 +122,7 @@ export function createPrivateLocalDelegation(input) {
|
|
|
71
122
|
}, {
|
|
72
123
|
agentId: task.agentId,
|
|
73
124
|
sessionId: task.sessionId,
|
|
125
|
+
delivery: task.delivery,
|
|
74
126
|
text: task.text,
|
|
75
127
|
requestId,
|
|
76
128
|
});
|
|
@@ -114,15 +166,22 @@ export function createPrivateLocalDelegation(input) {
|
|
|
114
166
|
if (mail.to.agentId !== input.agentId && mesh === undefined)
|
|
115
167
|
throw new Error("Local addressed mail recipient is not authorized");
|
|
116
168
|
const agents = mesh ?? (await members(new AbortController().signal));
|
|
169
|
+
const sender = mail.from;
|
|
117
170
|
const delegation = mail.replyTo.length === 0 ? fleet.authenticateReply(mail) : undefined;
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
171
|
+
const authenticatedUndeliverable = delegation?.reply.outcome === "undeliverable";
|
|
172
|
+
const senderIsMember = sender.kind === "agent" &&
|
|
173
|
+
agents.some(({ agentId }) => agentId === sender.agentId);
|
|
174
|
+
if (!senderIsMember && !authenticatedUndeliverable) {
|
|
175
|
+
if (sender.kind === "system" &&
|
|
176
|
+
mail.replyTo.length === 0 &&
|
|
177
|
+
mail.reply?.outcome !== "accepted")
|
|
178
|
+
throw new Error("Local reply does not match work this Agent sent");
|
|
122
179
|
throw new Error("Local mail sender is outside this project");
|
|
180
|
+
}
|
|
123
181
|
if (mail.replyTo.length > 0) {
|
|
124
182
|
const [destination] = mail.replyTo;
|
|
125
183
|
if (mail.replyTo.length !== 1 ||
|
|
184
|
+
mail.from.kind !== "agent" ||
|
|
126
185
|
destination?.agentId !== mail.from.agentId ||
|
|
127
186
|
destination.sessionId !== mail.from.sessionId)
|
|
128
187
|
throw new Error("Local delegated work must reply to its sender");
|
|
@@ -137,10 +196,12 @@ export function createPrivateLocalDelegation(input) {
|
|
|
137
196
|
if (delegation === undefined)
|
|
138
197
|
throw new Error("Local reply does not match work this Agent sent");
|
|
139
198
|
const replier = agents.find(({ agentId }) => agentId === delegation.reply.agentId);
|
|
140
|
-
if (replier === undefined && !
|
|
199
|
+
if (replier === undefined && !authenticatedUndeliverable)
|
|
141
200
|
throw new Error("Local reply sender is outside this project");
|
|
142
201
|
const meta = delegationReplyMeta(delegation, replier?.name ?? delegation.reply.agentId);
|
|
143
|
-
|
|
202
|
+
const accepted = await input.accept({ ...mail, meta }, meta, delegation.reply);
|
|
203
|
+
watchUnattended(accepted, mail.to.sessionId, delegation.reply);
|
|
204
|
+
return accepted;
|
|
144
205
|
}
|
|
145
|
-
return { send, recall, receive, outstanding: fleet.outstanding };
|
|
206
|
+
return { send, recall, receive, close, outstanding: fleet.outstanding };
|
|
146
207
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { gitRuleCoverage, rulePrefix } from "./private-local-exec-rules.js";
|
|
2
|
+
import { quote } from "shell-quote";
|
|
2
3
|
const gitHookGrantSubcommands = new Set([
|
|
3
4
|
"add",
|
|
4
5
|
"branch",
|
|
@@ -20,29 +21,62 @@ const gitHookGrantSubcommands = new Set([
|
|
|
20
21
|
export function localExecEscapeReview(request, cwd, gitGrant) {
|
|
21
22
|
if (request.escape === undefined)
|
|
22
23
|
throw new Error("An escape requires a justification");
|
|
23
|
-
const
|
|
24
|
-
JSON.stringify([request.executable, ...(request.args ?? [])]);
|
|
25
|
-
const prefix = request.executable === undefined ||
|
|
26
|
-
(request.executable === "git" && gitGrant === undefined)
|
|
24
|
+
const argv = request.executable === undefined
|
|
27
25
|
? undefined
|
|
28
|
-
:
|
|
26
|
+
: [request.executable, ...(request.args ?? [])];
|
|
27
|
+
const command = request.command ?? quote(argv ?? []);
|
|
28
|
+
const prefix = argv === undefined || (argv[0] === "git" && gitGrant === undefined)
|
|
29
|
+
? undefined
|
|
30
|
+
: rulePrefix(argv, gitGrant?.configuredRemotes);
|
|
29
31
|
const review = {
|
|
30
32
|
command,
|
|
31
33
|
cwd,
|
|
32
34
|
justification: request.escape.justification,
|
|
35
|
+
form: argv === undefined ? "shell" : "direct",
|
|
36
|
+
kind: argv?.[0] === "fungi" && argv[1] === "extension" && argv[2] === "add"
|
|
37
|
+
? "extension-install"
|
|
38
|
+
: "exec",
|
|
33
39
|
};
|
|
34
|
-
if (
|
|
40
|
+
if (argv !== undefined)
|
|
41
|
+
review.argv = argv;
|
|
42
|
+
if (prefix !== undefined && review.kind === "exec") {
|
|
35
43
|
review.prefix = prefix;
|
|
36
44
|
review.alwaysAllowCoverage =
|
|
37
45
|
prefix[0] === "git"
|
|
38
46
|
? gitRuleCoverage(prefix[1] ?? "")
|
|
39
47
|
: `Direct argv commands beginning ${JSON.stringify(prefix)} in this project; shell commands still ask.`;
|
|
48
|
+
if (prefix[0] === "git") {
|
|
49
|
+
const hookCapable = gitHookGrantSubcommands.has(prefix[1] ?? "");
|
|
50
|
+
const summary = (hookCapable
|
|
51
|
+
? "Git hooks in this project can run code as you, and this Agent can add hooks."
|
|
52
|
+
: "This Git command will run outside the sandbox, as you.") +
|
|
53
|
+
(prefix[1] === "push"
|
|
54
|
+
? " It can push, force-push or delete any branch."
|
|
55
|
+
: "") +
|
|
56
|
+
(["push", "fetch", "pull"].includes(prefix[1] ?? "")
|
|
57
|
+
? " Remotes can use your SSH keys."
|
|
58
|
+
: "");
|
|
59
|
+
const detail = (hookCapable
|
|
60
|
+
? "Git runs hook scripts from the folder the project's hooksPath setting names. That setting must already exist, but this Agent can add hook files there. A configured fsmonitor program in this folder may also be writable by this Agent."
|
|
61
|
+
: summary) +
|
|
62
|
+
(prefix[1] === "push"
|
|
63
|
+
? " A push grant can push all branches, force-update them or delete remote branches."
|
|
64
|
+
: "") +
|
|
65
|
+
(["push", "fetch", "pull"].includes(prefix[1] ?? "")
|
|
66
|
+
? " Remote URLs can run ssh with your HOME and keys."
|
|
67
|
+
: "");
|
|
68
|
+
review.risk = { summary, detail };
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
const summary = `Every \`${prefix.join(" ")} …\` command will run outside the sandbox, as you.`;
|
|
72
|
+
review.risk = { summary, detail: summary };
|
|
73
|
+
}
|
|
40
74
|
}
|
|
41
75
|
if (request.executable === "git") {
|
|
42
76
|
const hookCapable = gitHookGrantSubcommands.has(request.args?.[0] ?? "");
|
|
43
77
|
review.warning =
|
|
44
78
|
(hookCapable
|
|
45
|
-
?
|
|
79
|
+
? `Project hooks can run code as you ${review.prefix === undefined ? "when this command runs" : "under this grant"}. Only the hooksPath setting must pre-exist; this Agent can create missing hook files there. A configured fsmonitor path in the worktree may also be Agent-writable.`
|
|
46
80
|
: "This command runs as you outside the sandbox.") +
|
|
47
81
|
(request.args?.[0] === "push"
|
|
48
82
|
? " This grant can push all branches, force-update them, or delete remote branches."
|
|
@@ -51,7 +85,10 @@ export function localExecEscapeReview(request, cwd, gitGrant) {
|
|
|
51
85
|
? " Remote URLs can invoke ssh with your HOME and keys."
|
|
52
86
|
: "");
|
|
53
87
|
}
|
|
54
|
-
if (
|
|
88
|
+
if (review.kind === "extension-install")
|
|
89
|
+
review.alwaysAllowUnavailableReason =
|
|
90
|
+
"Extension installs require a fresh approval; no reusable rule applies.";
|
|
91
|
+
else if (review.prefix === undefined)
|
|
55
92
|
review.alwaysAllowUnavailableReason =
|
|
56
93
|
request.executable === undefined
|
|
57
94
|
? "Shell command text cannot match a saved argv rule; Approve once or Deny."
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const extensionArchiveNameSchema: z.ZodString;
|
|
3
|
+
export declare function deferExtensionArchiveNotices(home: string): void;
|
|
4
|
+
export declare function extensionArchiveNoticesAreDeferred(home: string): boolean;
|
|
5
|
+
export declare function acquireExtensionOwnerLock(root: string): Promise<() => Promise<void>>;
|
|
6
|
+
export declare function writeExtensionArchiveNotice(home: string, slug: "unversioned", filename: string, names: readonly string[]): Promise<void>;
|
|
7
|
+
export declare function takePendingExtensionArchiveNoticesUnderOwnerLock(home: string): Promise<string[]>;
|
|
8
|
+
export declare function takePendingExtensionArchiveNotices(home: string): Promise<string[]>;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { mkdir, readFile, readdir } from "node:fs/promises";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import lockfile from "proper-lockfile";
|
|
4
|
+
import writeFileAtomic from "write-file-atomic";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
export const extensionArchiveNameSchema = z
|
|
7
|
+
.string()
|
|
8
|
+
.trim()
|
|
9
|
+
.min(1)
|
|
10
|
+
.max(214)
|
|
11
|
+
.refine((name) => /^(?:@[a-z0-9._-]+\/)?[a-z0-9][a-z0-9._-]*$/iu.test(name));
|
|
12
|
+
const noticeSchema = z.strictObject({
|
|
13
|
+
version: z.literal(3),
|
|
14
|
+
slug: z.literal("unversioned"),
|
|
15
|
+
filename: z.string().regex(/^installed(?:-[2-9])?\.json$/u),
|
|
16
|
+
names: z.array(extensionArchiveNameSchema).max(20),
|
|
17
|
+
additionalNameCount: z.number().int().min(0),
|
|
18
|
+
shown: z.boolean(),
|
|
19
|
+
});
|
|
20
|
+
const deferredHomes = new Set();
|
|
21
|
+
function absent(cause) {
|
|
22
|
+
return (cause instanceof Error &&
|
|
23
|
+
"code" in cause &&
|
|
24
|
+
(cause.code === "ENOENT" || cause.code === "ENOTDIR"));
|
|
25
|
+
}
|
|
26
|
+
export function deferExtensionArchiveNotices(home) {
|
|
27
|
+
deferredHomes.add(resolve(home));
|
|
28
|
+
}
|
|
29
|
+
export function extensionArchiveNoticesAreDeferred(home) {
|
|
30
|
+
return deferredHomes.has(resolve(home));
|
|
31
|
+
}
|
|
32
|
+
export async function acquireExtensionOwnerLock(root) {
|
|
33
|
+
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
34
|
+
return lockfile.lock(root, {
|
|
35
|
+
realpath: false,
|
|
36
|
+
lockfilePath: join(root, ".owner.lock"),
|
|
37
|
+
stale: 30_000,
|
|
38
|
+
update: 10_000,
|
|
39
|
+
retries: { retries: 20, factor: 1.2, minTimeout: 25, maxTimeout: 100 },
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function formatNotice(notice) {
|
|
43
|
+
const archivePath = `~/.fungi/archive/${notice.slug}/extensions/${notice.filename}`;
|
|
44
|
+
if (notice.names.length === 0)
|
|
45
|
+
return `Extensions from an earlier version were set aside, but no extension names could be read. Re-add each with: fungi extension add <its source>. Archived registry: ${archivePath}.`;
|
|
46
|
+
const more = notice.additionalNameCount > 0
|
|
47
|
+
? `, and ${notice.additionalNameCount.toString()} more`
|
|
48
|
+
: "";
|
|
49
|
+
return `Extensions from an earlier version were set aside: ${notice.names.join(", ")}${more}. Re-add each with: fungi extension add <its source>. Archived registry: ${archivePath}.`;
|
|
50
|
+
}
|
|
51
|
+
export async function writeExtensionArchiveNotice(home, slug, filename, names) {
|
|
52
|
+
const notice = noticeSchema.parse({
|
|
53
|
+
version: 3,
|
|
54
|
+
slug,
|
|
55
|
+
filename,
|
|
56
|
+
names: names.slice(0, 20),
|
|
57
|
+
additionalNameCount: Math.max(0, names.length - 20),
|
|
58
|
+
shown: false,
|
|
59
|
+
});
|
|
60
|
+
const directory = join(resolve(home), ".fungi", "archive", slug, "extensions");
|
|
61
|
+
await writeFileAtomic(join(directory, `${filename}.notice.json`), `${JSON.stringify(notice)}\n`, { encoding: "utf8", fsync: true, mode: 0o600 });
|
|
62
|
+
}
|
|
63
|
+
export async function takePendingExtensionArchiveNoticesUnderOwnerLock(home) {
|
|
64
|
+
const directory = join(resolve(home), ".fungi", "archive", "unversioned", "extensions");
|
|
65
|
+
let names;
|
|
66
|
+
try {
|
|
67
|
+
names = await readdir(directory);
|
|
68
|
+
}
|
|
69
|
+
catch (cause) {
|
|
70
|
+
if (absent(cause))
|
|
71
|
+
return [];
|
|
72
|
+
throw cause;
|
|
73
|
+
}
|
|
74
|
+
const notices = [];
|
|
75
|
+
for (const name of names
|
|
76
|
+
.filter((candidate) => /^installed(?:-[2-9])?\.json\.notice\.json$/u.test(candidate))
|
|
77
|
+
.sort()) {
|
|
78
|
+
const path = join(directory, name);
|
|
79
|
+
let value;
|
|
80
|
+
try {
|
|
81
|
+
value = JSON.parse(await readFile(path, "utf8"));
|
|
82
|
+
}
|
|
83
|
+
catch (cause) {
|
|
84
|
+
if (absent(cause) || cause instanceof SyntaxError)
|
|
85
|
+
continue;
|
|
86
|
+
throw cause;
|
|
87
|
+
}
|
|
88
|
+
const parsed = noticeSchema.safeParse(value);
|
|
89
|
+
if (!parsed.success || parsed.data.shown)
|
|
90
|
+
continue;
|
|
91
|
+
const archivedName = name.replace(/\.notice\.json$/u, "");
|
|
92
|
+
if (parsed.data.filename !== archivedName)
|
|
93
|
+
continue;
|
|
94
|
+
await writeFileAtomic(path, `${JSON.stringify({ ...parsed.data, shown: true })}\n`, { encoding: "utf8", fsync: true, mode: 0o600 });
|
|
95
|
+
notices.push(formatNotice(parsed.data));
|
|
96
|
+
}
|
|
97
|
+
return notices;
|
|
98
|
+
}
|
|
99
|
+
export async function takePendingExtensionArchiveNotices(home) {
|
|
100
|
+
const root = join(resolve(home), ".fungi", "extensions");
|
|
101
|
+
const release = await acquireExtensionOwnerLock(root);
|
|
102
|
+
try {
|
|
103
|
+
return await takePendingExtensionArchiveNoticesUnderOwnerLock(home);
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
await release();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** One vocabulary for installed declarations and the activation reader. */
|
|
3
|
+
export declare const capabilities: readonly ["state", "attention", "settled", "send", "timers", "commands", "projection", "run_tools", "files", "prompt", "compaction_handoff"];
|
|
4
|
+
export declare const activationsSchema: z.ZodObject<{
|
|
5
|
+
projects: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
6
|
+
agents: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
7
|
+
revision: z.ZodString;
|
|
8
|
+
capabilities: z.ZodArray<z.ZodEnum<{
|
|
9
|
+
state: "state";
|
|
10
|
+
attention: "attention";
|
|
11
|
+
settled: "settled";
|
|
12
|
+
send: "send";
|
|
13
|
+
timers: "timers";
|
|
14
|
+
commands: "commands";
|
|
15
|
+
projection: "projection";
|
|
16
|
+
run_tools: "run_tools";
|
|
17
|
+
files: "files";
|
|
18
|
+
prompt: "prompt";
|
|
19
|
+
compaction_handoff: "compaction_handoff";
|
|
20
|
+
}>>;
|
|
21
|
+
skills: z.ZodBoolean;
|
|
22
|
+
}, z.core.$strict>>>;
|
|
23
|
+
}, z.core.$strict>;
|
|
24
|
+
/** Only refused activations belong to the home upgrader, not installed.json. */
|
|
25
|
+
export declare function incompatibleLocalExtensionActivations(home: string): Promise<readonly string[]>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
/** One vocabulary for installed declarations and the activation reader. */
|
|
5
|
+
export const capabilities = [
|
|
6
|
+
"state",
|
|
7
|
+
"attention",
|
|
8
|
+
"settled",
|
|
9
|
+
"send",
|
|
10
|
+
"timers",
|
|
11
|
+
"commands",
|
|
12
|
+
"projection",
|
|
13
|
+
"run_tools",
|
|
14
|
+
"files",
|
|
15
|
+
"prompt",
|
|
16
|
+
"compaction_handoff",
|
|
17
|
+
];
|
|
18
|
+
export const activationsSchema = z.strictObject({
|
|
19
|
+
projects: z.record(z.string(), z.record(z.string(), z.string())),
|
|
20
|
+
agents: z.record(z.string(), z.record(z.string(), z.strictObject({
|
|
21
|
+
revision: z.string(),
|
|
22
|
+
capabilities: z.array(z.enum(capabilities)),
|
|
23
|
+
/** The opt-in to the revision's skills folder; separate from tools. */
|
|
24
|
+
skills: z.boolean(),
|
|
25
|
+
}))),
|
|
26
|
+
});
|
|
27
|
+
/** Only refused activations belong to the home upgrader, not installed.json. */
|
|
28
|
+
export async function incompatibleLocalExtensionActivations(home) {
|
|
29
|
+
const path = join(resolve(home), ".fungi", "extensions", "activations.json");
|
|
30
|
+
let text;
|
|
31
|
+
try {
|
|
32
|
+
text = await readFile(path, "utf8");
|
|
33
|
+
}
|
|
34
|
+
catch (cause) {
|
|
35
|
+
if (cause instanceof Error &&
|
|
36
|
+
"code" in cause &&
|
|
37
|
+
(cause.code === "ENOENT" || cause.code === "ENOTDIR"))
|
|
38
|
+
return [];
|
|
39
|
+
throw cause;
|
|
40
|
+
}
|
|
41
|
+
let value;
|
|
42
|
+
try {
|
|
43
|
+
value = JSON.parse(text);
|
|
44
|
+
}
|
|
45
|
+
catch (cause) {
|
|
46
|
+
if (cause instanceof SyntaxError)
|
|
47
|
+
return [path];
|
|
48
|
+
throw cause;
|
|
49
|
+
}
|
|
50
|
+
return activationsSchema.safeParse(value).success ? [] : [path];
|
|
51
|
+
}
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
export type ServiceProcessIdentity = Readonly<{
|
|
2
|
+
kind: "ours";
|
|
3
|
+
evidence: "start-time" | "command-line";
|
|
4
|
+
}> | Readonly<{
|
|
5
|
+
kind: "other";
|
|
6
|
+
}> | Readonly<{
|
|
7
|
+
kind: "gone";
|
|
8
|
+
}>;
|
|
1
9
|
/**
|
|
2
10
|
* A process-start discriminator. `unverifiable` means no sufficiently precise
|
|
3
11
|
* reader is implemented on this platform.
|
|
@@ -16,5 +24,11 @@ export type ProcessStart = Readonly<{
|
|
|
16
24
|
* accepted as signal authority. No sub-second macOS reader is implemented.
|
|
17
25
|
*/
|
|
18
26
|
export declare function readProcessStart(pid: number): Promise<ProcessStart>;
|
|
27
|
+
/** Never signal a PID merely because a lease names it. */
|
|
28
|
+
export declare function identifyLocalServiceProcess(claim: Readonly<{
|
|
29
|
+
pid: number;
|
|
30
|
+
startedAt: string;
|
|
31
|
+
directory: string;
|
|
32
|
+
}>): Promise<ServiceProcessIdentity>;
|
|
19
33
|
/** The owner's start discriminator; an unverifiable marker never permits signalling. */
|
|
20
34
|
export declare function readOwnProcessStart(): Promise<string>;
|
|
@@ -1,6 +1,25 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
1
2
|
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { promisify } from "node:util";
|
|
2
4
|
import { z } from "zod";
|
|
3
5
|
const procStartSchema = z.string().regex(/^\d+$/u);
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
/** Signal 0 checks existence, not identity; EPERM is still a live process. */
|
|
8
|
+
function processIsGone(pid) {
|
|
9
|
+
try {
|
|
10
|
+
process.kill(pid, 0);
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
catch (cause) {
|
|
14
|
+
if (cause instanceof Error && "code" in cause) {
|
|
15
|
+
if (cause.code === "ESRCH")
|
|
16
|
+
return true;
|
|
17
|
+
if (cause.code === "EPERM")
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
throw cause;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
4
23
|
/**
|
|
5
24
|
* Linux reads proc(5)'s `starttime` (field 22 of `/proc/<pid>/stat`, in clock
|
|
6
25
|
* ticks since boot). macOS's ps lstart is only second-resolution and is not
|
|
@@ -29,6 +48,38 @@ export async function readProcessStart(pid) {
|
|
|
29
48
|
}
|
|
30
49
|
return { kind: "unverifiable" };
|
|
31
50
|
}
|
|
51
|
+
/** Never signal a PID merely because a lease names it. */
|
|
52
|
+
export async function identifyLocalServiceProcess(claim) {
|
|
53
|
+
const start = await readProcessStart(claim.pid);
|
|
54
|
+
if (start.kind === "gone")
|
|
55
|
+
return { kind: "gone" };
|
|
56
|
+
if (start.kind === "running")
|
|
57
|
+
return start.start === claim.startedAt
|
|
58
|
+
? { kind: "ours", evidence: "start-time" }
|
|
59
|
+
: { kind: "other" };
|
|
60
|
+
if (process.platform !== "darwin")
|
|
61
|
+
return { kind: "other" };
|
|
62
|
+
if (processIsGone(claim.pid))
|
|
63
|
+
return { kind: "gone" };
|
|
64
|
+
// ps is only a corroborating channel after the caller has checked the
|
|
65
|
+
// matching service.json and owner.sqlite lease. Its output is bounded; an
|
|
66
|
+
// unreadable or different argv never grants signal authority.
|
|
67
|
+
let argv;
|
|
68
|
+
try {
|
|
69
|
+
({ stdout: argv } = await execFileAsync("ps", ["-p", String(claim.pid), "-o", "args="], { timeout: 2_000, maxBuffer: 4_096 }));
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return processIsGone(claim.pid) ? { kind: "gone" } : { kind: "other" };
|
|
73
|
+
}
|
|
74
|
+
const marker = `--internal-service ${claim.directory}`;
|
|
75
|
+
const offset = argv.indexOf(marker);
|
|
76
|
+
return offset >= 0 &&
|
|
77
|
+
(offset === 0 || /\s/u.test(argv[offset - 1] ?? "")) &&
|
|
78
|
+
(offset + marker.length === argv.trimEnd().length ||
|
|
79
|
+
/\s/u.test(argv[offset + marker.length] ?? ""))
|
|
80
|
+
? { kind: "ours", evidence: "command-line" }
|
|
81
|
+
: { kind: "other" };
|
|
82
|
+
}
|
|
32
83
|
/** The owner's start discriminator; an unverifiable marker never permits signalling. */
|
|
33
84
|
export async function readOwnProcessStart() {
|
|
34
85
|
const own = await readProcessStart(process.pid);
|
|
@@ -80,6 +80,8 @@ export type PrivateLocalRuntimeHostOptions = Readonly<{
|
|
|
80
80
|
name?: string | undefined;
|
|
81
81
|
/** The Agent that created this one; recorded once with its identity. Absent when a human made it. */
|
|
82
82
|
createdBy?: string | undefined;
|
|
83
|
+
/** The Session whose create request is durably bound to this Local receipt. */
|
|
84
|
+
createdBySessionId?: string | undefined;
|
|
83
85
|
/** Immutable project-relative folder authority, inherited by child Agents. */
|
|
84
86
|
folder?: string | undefined;
|
|
85
87
|
/** Persist before constructing the Agent, which can recover durable work. */
|
|
@@ -172,8 +174,12 @@ export type PrivateLocalCreateStore = Readonly<{
|
|
|
172
174
|
agentId: LocalStoreId;
|
|
173
175
|
databasePath: string;
|
|
174
176
|
}>;
|
|
177
|
+
export type PrivateLocalCreateCreator = Readonly<{
|
|
178
|
+
agentId: string;
|
|
179
|
+
sessionId: string;
|
|
180
|
+
}>;
|
|
175
181
|
/** Resolve an Agent create request through its durable Local receipt. */
|
|
176
|
-
export declare function resolvePrivateLocalCreateStore(root: string, requestId: string): Promise<PrivateLocalCreateStore>;
|
|
182
|
+
export declare function resolvePrivateLocalCreateStore(root: string, requestId: string, creator?: PrivateLocalCreateCreator): Promise<PrivateLocalCreateStore>;
|
|
177
183
|
/** Build the fixed source read by Shiitake once for each local run. */
|
|
178
184
|
export declare function createPrivateLocalInstructionSource(port: Awaited<ReturnType<typeof createPrivateInvocationFilesPort>>): InstructionSource;
|
|
179
185
|
export declare function createPrivateLocalModelSource(models: Models, providerId: string | undefined, modelId: string | undefined, providerCacheKey: string, selectedOwnerReasoning?: ThinkingLevel, defaults?: Readonly<{
|