@fungi.computer/truffle 0.3.0 → 0.4.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-extensions.js +108 -33
- package/dist/local-login.js +9 -3
- package/dist/local-upgrade.d.ts +63 -0
- package/dist/local-upgrade.js +358 -0
- package/dist/private-local-agent-control.d.ts +3 -0
- package/dist/private-local-agent-control.js +5 -1
- package/dist/private-local-delegation.js +12 -5
- package/dist/private-local-exec-review.d.ts +3 -1
- package/dist/private-local-exec-review.js +66 -17
- package/dist/private-local-exec-rules.d.ts +5 -3
- package/dist/private-local-exec-rules.js +30 -9
- 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 +16 -1
- package/dist/private-local-runtime-host.js +92 -37
- package/dist/private-local-service-discovery.js +2 -3
- package/dist/private-local-service-listener.js +1 -1
- package/dist/private-local-service-log.d.ts +10 -0
- package/dist/private-local-service-log.js +3 -0
- package/dist/private-local-service-owner.d.ts +1 -1
- package/dist/private-local-service-owner.js +22 -8
- package/dist/private-local-service-termination.js +12 -11
- package/dist/private-local-service.js +12 -0
- package/dist/private-local-session-discovery.d.ts +6 -0
- package/dist/private-local-session-discovery.js +31 -0
- package/dist/private-mcp-form-contract.d.ts +15 -0
- package/dist/private-mcp-form-contract.js +8 -1
- package/dist/private-mcp-form-session.d.ts +32 -0
- package/dist/private-mcp-form-session.js +8 -5
- package/package.json +20 -13
|
@@ -114,15 +114,22 @@ export function createPrivateLocalDelegation(input) {
|
|
|
114
114
|
if (mail.to.agentId !== input.agentId && mesh === undefined)
|
|
115
115
|
throw new Error("Local addressed mail recipient is not authorized");
|
|
116
116
|
const agents = mesh ?? (await members(new AbortController().signal));
|
|
117
|
+
const sender = mail.from;
|
|
117
118
|
const delegation = mail.replyTo.length === 0 ? fleet.authenticateReply(mail) : undefined;
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
const authenticatedUndeliverable = delegation?.reply.outcome === "undeliverable";
|
|
120
|
+
const senderIsMember = sender.kind === "agent" &&
|
|
121
|
+
agents.some(({ agentId }) => agentId === sender.agentId);
|
|
122
|
+
if (!senderIsMember && !authenticatedUndeliverable) {
|
|
123
|
+
if (sender.kind === "system" &&
|
|
124
|
+
mail.replyTo.length === 0 &&
|
|
125
|
+
mail.reply?.outcome !== "accepted")
|
|
126
|
+
throw new Error("Local reply does not match work this Agent sent");
|
|
122
127
|
throw new Error("Local mail sender is outside this project");
|
|
128
|
+
}
|
|
123
129
|
if (mail.replyTo.length > 0) {
|
|
124
130
|
const [destination] = mail.replyTo;
|
|
125
131
|
if (mail.replyTo.length !== 1 ||
|
|
132
|
+
mail.from.kind !== "agent" ||
|
|
126
133
|
destination?.agentId !== mail.from.agentId ||
|
|
127
134
|
destination.sessionId !== mail.from.sessionId)
|
|
128
135
|
throw new Error("Local delegated work must reply to its sender");
|
|
@@ -137,7 +144,7 @@ export function createPrivateLocalDelegation(input) {
|
|
|
137
144
|
if (delegation === undefined)
|
|
138
145
|
throw new Error("Local reply does not match work this Agent sent");
|
|
139
146
|
const replier = agents.find(({ agentId }) => agentId === delegation.reply.agentId);
|
|
140
|
-
if (replier === undefined && !
|
|
147
|
+
if (replier === undefined && !authenticatedUndeliverable)
|
|
141
148
|
throw new Error("Local reply sender is outside this project");
|
|
142
149
|
const meta = delegationReplyMeta(delegation, replier?.name ?? delegation.reply.agentId);
|
|
143
150
|
return input.accept({ ...mail, meta }, meta, delegation.reply);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { rulePrefix } from "./private-local-exec-rules.js";
|
|
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",
|
|
@@ -17,34 +18,82 @@ const gitHookGrantSubcommands = new Set([
|
|
|
17
18
|
"tag",
|
|
18
19
|
]);
|
|
19
20
|
/** Build the host's review before the approval record is staged or displayed. */
|
|
20
|
-
export function localExecEscapeReview(request, cwd) {
|
|
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
|
|
24
|
+
const argv = request.executable === undefined
|
|
26
25
|
? undefined
|
|
27
|
-
:
|
|
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);
|
|
28
31
|
const review = {
|
|
29
32
|
command,
|
|
30
33
|
cwd,
|
|
31
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",
|
|
32
39
|
};
|
|
33
|
-
if (
|
|
40
|
+
if (argv !== undefined)
|
|
41
|
+
review.argv = argv;
|
|
42
|
+
if (prefix !== undefined && review.kind === "exec") {
|
|
34
43
|
review.prefix = prefix;
|
|
44
|
+
review.alwaysAllowCoverage =
|
|
45
|
+
prefix[0] === "git"
|
|
46
|
+
? gitRuleCoverage(prefix[1] ?? "")
|
|
47
|
+
: `Direct argv commands beginning ${JSON.stringify(prefix)} in this project; shell commands still ask.`;
|
|
35
48
|
if (prefix[0] === "git") {
|
|
36
49
|
const hookCapable = gitHookGrantSubcommands.has(prefix[1] ?? "");
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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 };
|
|
47
73
|
}
|
|
48
74
|
}
|
|
75
|
+
if (request.executable === "git") {
|
|
76
|
+
const hookCapable = gitHookGrantSubcommands.has(request.args?.[0] ?? "");
|
|
77
|
+
review.warning =
|
|
78
|
+
(hookCapable
|
|
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.`
|
|
80
|
+
: "This command runs as you outside the sandbox.") +
|
|
81
|
+
(request.args?.[0] === "push"
|
|
82
|
+
? " This grant can push all branches, force-update them, or delete remote branches."
|
|
83
|
+
: "") +
|
|
84
|
+
(["push", "fetch", "pull"].includes(request.args?.[0] ?? "")
|
|
85
|
+
? " Remote URLs can invoke ssh with your HOME and keys."
|
|
86
|
+
: "");
|
|
87
|
+
}
|
|
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)
|
|
92
|
+
review.alwaysAllowUnavailableReason =
|
|
93
|
+
request.executable === undefined
|
|
94
|
+
? "Shell command text cannot match a saved argv rule; Approve once or Deny."
|
|
95
|
+
: request.executable === "git" && gitGrant === undefined
|
|
96
|
+
? "A protected project Git repository is unavailable; no reusable Git rule can match."
|
|
97
|
+
: "This command or option cannot match a safe reusable rule; Approve once or Deny.";
|
|
49
98
|
return review;
|
|
50
99
|
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
/** Offer only direct
|
|
2
|
-
export declare function rulePrefix(argv: readonly string[]): string[] | undefined;
|
|
1
|
+
/** Offer only direct commands that this same matcher can authorize. */
|
|
2
|
+
export declare function rulePrefix(argv: readonly string[], configuredRemotes?: ReadonlySet<string>): string[] | undefined;
|
|
3
|
+
/** Text for the same Git option profile that gates matching, not a second list. */
|
|
4
|
+
export declare function gitRuleCoverage(subcommand: string): string;
|
|
3
5
|
/** One host-owned project file; every Agent in the project sees the same rules. */
|
|
4
6
|
export declare function localExecRules(project: string): {
|
|
5
7
|
list: () => string[][];
|
|
6
|
-
add: (prefix: readonly string[]) =>
|
|
8
|
+
add: (prefix: readonly string[]) => import("@fungi.computer/cairn").CairnId<"local-exec-rules", "rule", 1>;
|
|
7
9
|
remove: (prefix: readonly string[]) => void;
|
|
8
10
|
match: (argv: readonly string[], configuredRemotes?: ReadonlySet<string>) => {
|
|
9
11
|
ruleId: import("@fungi.computer/cairn").CairnId<"local-exec-rules", "rule", 1>;
|
|
@@ -62,7 +62,7 @@ const gitOptions = {
|
|
|
62
62
|
"-s",
|
|
63
63
|
"--signoff",
|
|
64
64
|
],
|
|
65
|
-
values: ["-m", "--message"],
|
|
65
|
+
values: ["-m", "--message", "-am"],
|
|
66
66
|
},
|
|
67
67
|
diff: {
|
|
68
68
|
flags: [
|
|
@@ -250,8 +250,8 @@ function validPrefix(prefix) {
|
|
|
250
250
|
prefix.length === 2 &&
|
|
251
251
|
gitBuiltins.has(prefix[1] ?? ""))));
|
|
252
252
|
}
|
|
253
|
-
/** Offer only direct
|
|
254
|
-
export function rulePrefix(argv) {
|
|
253
|
+
/** Offer only direct commands that this same matcher can authorize. */
|
|
254
|
+
export function rulePrefix(argv, configuredRemotes) {
|
|
255
255
|
if (argv.length === 0 ||
|
|
256
256
|
argv[0]?.startsWith(".") ||
|
|
257
257
|
argv[0]?.includes("/") ||
|
|
@@ -260,7 +260,9 @@ export function rulePrefix(argv) {
|
|
|
260
260
|
return undefined;
|
|
261
261
|
if (basename(argv[0] ?? "") === "git") {
|
|
262
262
|
const prefix = argv.slice(0, 2);
|
|
263
|
-
return validPrefix(prefix)
|
|
263
|
+
return validPrefix(prefix) && safeGitCommand(argv, configuredRemotes)
|
|
264
|
+
? prefix
|
|
265
|
+
: undefined;
|
|
264
266
|
}
|
|
265
267
|
if (basename(argv[0] ?? "") === "fungi" &&
|
|
266
268
|
argv[1] === "extension" &&
|
|
@@ -269,6 +271,21 @@ export function rulePrefix(argv) {
|
|
|
269
271
|
const parsed = prefixSchema.safeParse(argv.slice(0, 2));
|
|
270
272
|
return parsed.success && validPrefix(parsed.data) ? parsed.data : undefined;
|
|
271
273
|
}
|
|
274
|
+
/** Text for the same Git option profile that gates matching, not a second list. */
|
|
275
|
+
export function gitRuleCoverage(subcommand) {
|
|
276
|
+
const profile = gitProfiles.get(subcommand);
|
|
277
|
+
if (profile === undefined)
|
|
278
|
+
throw new Error("No reusable Git option profile");
|
|
279
|
+
const flags = profile.flags.filter((flag) => flag !== "--").join(", ");
|
|
280
|
+
const values = profile.values.map((option) => `${option} VALUE`).join(", ");
|
|
281
|
+
const remote = subcommand === "push" ||
|
|
282
|
+
subcommand === "fetch" ||
|
|
283
|
+
subcommand === "pull" ||
|
|
284
|
+
subcommand === "remote"
|
|
285
|
+
? " Remote operands must be configured names or permitted explicit URLs."
|
|
286
|
+
: "";
|
|
287
|
+
return `Direct argv git ${subcommand} for Workspace Agents in this protected project repository. Only these options are reusable: ${flags || "none"}${values ? `; ${values}` : ""}; -- and permitted operands.${remote} Shell commands and unlisted Git options still ask.`;
|
|
288
|
+
}
|
|
272
289
|
function safeRemote(remote, configuredRemotes) {
|
|
273
290
|
if (remote === undefined)
|
|
274
291
|
return true;
|
|
@@ -378,14 +395,18 @@ export function localExecRules(project) {
|
|
|
378
395
|
const parsed = prefixSchema.parse(prefix);
|
|
379
396
|
if (!validPrefix(parsed))
|
|
380
397
|
throw new Error("Unsafe Always-allow prefix");
|
|
398
|
+
let ruleId;
|
|
381
399
|
lockedUpdate(project, () => {
|
|
382
400
|
const existing = readProject(project).rules;
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
401
|
+
ruleId = existing.find((rule) => JSON.stringify(rule.prefix) === JSON.stringify(parsed))?.id;
|
|
402
|
+
if (ruleId === undefined) {
|
|
403
|
+
ruleId = projectRuleIds.rule.generate();
|
|
404
|
+
writeProject(project, [...existing, { id: ruleId, prefix: parsed }]);
|
|
405
|
+
}
|
|
388
406
|
});
|
|
407
|
+
if (ruleId === undefined)
|
|
408
|
+
throw new Error("Exec rule was not persisted");
|
|
409
|
+
return ruleId;
|
|
389
410
|
};
|
|
390
411
|
const remove = (prefix) => {
|
|
391
412
|
const parsed = prefixSchema.parse(prefix);
|
|
@@ -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);
|
|
@@ -48,6 +48,13 @@ export type PrivateLocalTools = Readonly<{
|
|
|
48
48
|
/** Background Computer processes, scoped per Session; the host closes them. */
|
|
49
49
|
processes: PrivateLocalComputerProcesses;
|
|
50
50
|
}>;
|
|
51
|
+
export type ExecApprovalDecision = {
|
|
52
|
+
agentId: string;
|
|
53
|
+
recordId: string;
|
|
54
|
+
decision: "approve" | "deny";
|
|
55
|
+
scope?: "once" | "always";
|
|
56
|
+
ruleId?: string;
|
|
57
|
+
};
|
|
51
58
|
/** Inputs for the one private local Agent opener. */
|
|
52
59
|
export type PrivateLocalRuntimeHostOptions = Readonly<{
|
|
53
60
|
/** Service-selected host configuration, independent of this Agent's Files root. */
|
|
@@ -63,6 +70,8 @@ export type PrivateLocalRuntimeHostOptions = Readonly<{
|
|
|
63
70
|
executable: string;
|
|
64
71
|
ruleId: string;
|
|
65
72
|
}>) => void;
|
|
73
|
+
/** The human's committed approval choice, without command text or secrets. */
|
|
74
|
+
onExecApprovalDecision?: (event: Readonly<ExecApprovalDecision>) => void;
|
|
66
75
|
/** Existing Agent database to reopen instead of creating a fresh local store. */
|
|
67
76
|
databasePath?: string | undefined;
|
|
68
77
|
/** Stable Local store identity for replaying one Agent create request. */
|
|
@@ -71,6 +80,8 @@ export type PrivateLocalRuntimeHostOptions = Readonly<{
|
|
|
71
80
|
name?: string | undefined;
|
|
72
81
|
/** The Agent that created this one; recorded once with its identity. Absent when a human made it. */
|
|
73
82
|
createdBy?: string | undefined;
|
|
83
|
+
/** The Session whose create request is durably bound to this Local receipt. */
|
|
84
|
+
createdBySessionId?: string | undefined;
|
|
74
85
|
/** Immutable project-relative folder authority, inherited by child Agents. */
|
|
75
86
|
folder?: string | undefined;
|
|
76
87
|
/** Persist before constructing the Agent, which can recover durable work. */
|
|
@@ -163,8 +174,12 @@ export type PrivateLocalCreateStore = Readonly<{
|
|
|
163
174
|
agentId: LocalStoreId;
|
|
164
175
|
databasePath: string;
|
|
165
176
|
}>;
|
|
177
|
+
export type PrivateLocalCreateCreator = Readonly<{
|
|
178
|
+
agentId: string;
|
|
179
|
+
sessionId: string;
|
|
180
|
+
}>;
|
|
166
181
|
/** Resolve an Agent create request through its durable Local receipt. */
|
|
167
|
-
export declare function resolvePrivateLocalCreateStore(root: string, requestId: string): Promise<PrivateLocalCreateStore>;
|
|
182
|
+
export declare function resolvePrivateLocalCreateStore(root: string, requestId: string, creator?: PrivateLocalCreateCreator): Promise<PrivateLocalCreateStore>;
|
|
168
183
|
/** Build the fixed source read by Shiitake once for each local run. */
|
|
169
184
|
export declare function createPrivateLocalInstructionSource(port: Awaited<ReturnType<typeof createPrivateInvocationFilesPort>>): InstructionSource;
|
|
170
185
|
export declare function createPrivateLocalModelSource(models: Models, providerId: string | undefined, modelId: string | undefined, providerCacheKey: string, selectedOwnerReasoning?: ThinkingLevel, defaults?: Readonly<{
|