@fungi.computer/truffle 0.3.1 → 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.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 +48 -18
- package/dist/private-local-service-discovery.js +2 -3
- package/dist/private-local-service-owner.js +19 -7
- 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 +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/package.json +20 -13
|
@@ -10,6 +10,7 @@ import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
|
|
|
10
10
|
import { setTimeout as delay } from "node:timers/promises";
|
|
11
11
|
import { AgentCreateRefused, AgentArchiveRefused, } from "@fungi.computer/shiitake/agent-control";
|
|
12
12
|
import { localExtensions, LocalExtensionError, } from "./local-extensions.js";
|
|
13
|
+
import { deferExtensionArchiveNotices } from "./private-local-extension-archive-notice.js";
|
|
13
14
|
import { readFile, realpath } from "node:fs/promises";
|
|
14
15
|
import writeFileAtomic from "write-file-atomic";
|
|
15
16
|
import { z } from "zod";
|
|
@@ -72,7 +73,11 @@ readRecoveryManifest = (path) => readFile(path, "utf8"),
|
|
|
72
73
|
onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot(), meshLinksFactory = createLocalMeshLinks, onExecRuleMatch = () => undefined,
|
|
73
74
|
/** Supplied by the listener after startup; individual Agents never close it. */
|
|
74
75
|
serviceNative, onExecApprovalDecision = () => undefined) {
|
|
75
|
-
|
|
76
|
+
deferExtensionArchiveNotices(configRoot.home);
|
|
77
|
+
const extensions = suppliedExtensions ??
|
|
78
|
+
localExtensions({
|
|
79
|
+
home: configRoot.home,
|
|
80
|
+
});
|
|
76
81
|
const recoveryPath = recoveryDirectory === undefined
|
|
77
82
|
? undefined
|
|
78
83
|
: resolve(recoveryDirectory, "owners.json");
|
|
@@ -176,8 +181,9 @@ serviceNative, onExecApprovalDecision = () => undefined) {
|
|
|
176
181
|
envelope.key !== mail.key ||
|
|
177
182
|
mail.to.agentId !== address.agentId)
|
|
178
183
|
return "refused";
|
|
179
|
-
if (mail.from.
|
|
180
|
-
!(mail.
|
|
184
|
+
if (mail.from.kind === "system" &&
|
|
185
|
+
!(mail.reply?.outcome === "undeliverable" &&
|
|
186
|
+
mail.reply.reason === "recipient_offline"))
|
|
181
187
|
return "refused";
|
|
182
188
|
const sessionlessAcceptance = mail.to.sessionId === undefined;
|
|
183
189
|
// Only select the physical store here. Never bypass that runtime's
|
|
@@ -229,7 +235,9 @@ serviceNative, onExecApprovalDecision = () => undefined) {
|
|
|
229
235
|
for (const project of projects)
|
|
230
236
|
selections.set(project, await readProjectFungiMcpSelection(project));
|
|
231
237
|
const candidate = {
|
|
232
|
-
load: await localExtensions({
|
|
238
|
+
load: await localExtensions({
|
|
239
|
+
home: configRoot.home,
|
|
240
|
+
}).captureAgent(),
|
|
233
241
|
mcp: selections,
|
|
234
242
|
clientPath: path,
|
|
235
243
|
};
|
|
@@ -801,8 +809,9 @@ serviceNative, onExecApprovalDecision = () => undefined) {
|
|
|
801
809
|
const control = directory(invocationRoot);
|
|
802
810
|
return {
|
|
803
811
|
deliver: async (mail) => {
|
|
812
|
+
const senderAgentId = mail.from.kind === "agent" ? mail.from.agentId : undefined;
|
|
804
813
|
const remote = [...meshDirectories.values()].find(({ record }) => record.invocationRoot === invocationRoot &&
|
|
805
|
-
record.bindings.some((binding) => binding.agentId ===
|
|
814
|
+
record.bindings.some((binding) => binding.agentId === senderAgentId));
|
|
806
815
|
if (remote !== undefined) {
|
|
807
816
|
if (!remote.agents.some((agent) => agent.agentId === mail.to.agentId))
|
|
808
817
|
throw new Error("Mesh mail recipient is outside this Team");
|
|
@@ -1001,9 +1010,10 @@ serviceNative, onExecApprovalDecision = () => undefined) {
|
|
|
1001
1010
|
return model === null ? agent : { ...agent, model };
|
|
1002
1011
|
});
|
|
1003
1012
|
},
|
|
1004
|
-
create: async ({ requestId, name, model, extensions: requested = [], createdBy }, signal) => {
|
|
1013
|
+
create: async ({ requestId, name, model, extensions: requested = [], createdBy, createdBySessionId, }, signal) => {
|
|
1005
1014
|
ensureRunning();
|
|
1006
1015
|
signal.throwIfAborted();
|
|
1016
|
+
z.string().min(1).max(256).parse(createdBySessionId);
|
|
1007
1017
|
// An Agent creating an Agent cannot give it more than it holds. Refuse
|
|
1008
1018
|
// before anything exists; never trim the request.
|
|
1009
1019
|
const creatorDatabase = (await targets(signal)).get(createdBy);
|
|
@@ -1021,7 +1031,8 @@ serviceNative, onExecApprovalDecision = () => undefined) {
|
|
|
1021
1031
|
throw error;
|
|
1022
1032
|
}
|
|
1023
1033
|
const key = `create:${JSON.stringify([invocationRoot, requestId])}`;
|
|
1024
|
-
const
|
|
1034
|
+
const creator = { agentId: createdBy, sessionId: createdBySessionId };
|
|
1035
|
+
const createStore = await resolvePrivateLocalCreateStore(invocationRoot, requestId, creator);
|
|
1025
1036
|
signal.throwIfAborted();
|
|
1026
1037
|
ensureRunning();
|
|
1027
1038
|
const databasePath = createStore.databasePath;
|
|
@@ -1045,6 +1056,7 @@ serviceNative, onExecApprovalDecision = () => undefined) {
|
|
|
1045
1056
|
createRequestId: requestId,
|
|
1046
1057
|
name,
|
|
1047
1058
|
createdBy,
|
|
1059
|
+
createdBySessionId,
|
|
1048
1060
|
mode: creatorProfile.mode,
|
|
1049
1061
|
folder: creatorProfile.folder,
|
|
1050
1062
|
agentControl: directory(invocationRoot),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { setTimeout as delay } from "node:timers/promises";
|
|
2
|
-
import {
|
|
2
|
+
import { identifyLocalServiceProcess } from "./private-local-process-identity.js";
|
|
3
3
|
import { readServiceLeaseOwner, } from "./private-local-service-lease.js";
|
|
4
4
|
/** How long a SIGTERM'd service may drain its owners before it is killed. */
|
|
5
5
|
const terminationGraceMs = 5_000;
|
|
@@ -8,11 +8,11 @@ const terminationPollMs = 50;
|
|
|
8
8
|
* Whether `service` is still the process running under its PID. A PID whose
|
|
9
9
|
* start differs is refused. A separate read and signal still has a race window.
|
|
10
10
|
*/
|
|
11
|
-
async function isServiceProcess(service) {
|
|
12
|
-
const current = await
|
|
13
|
-
if (current.kind === "
|
|
14
|
-
throw new Error(`
|
|
15
|
-
return current.kind === "
|
|
11
|
+
async function isServiceProcess(directory, service) {
|
|
12
|
+
const current = await identifyLocalServiceProcess({ ...service, directory });
|
|
13
|
+
if (current.kind === "other" && process.platform !== "linux")
|
|
14
|
+
throw new Error(`Cannot safely stop Fungi service pid ${String(service.pid)} in ${directory}: its command line does not identify this service. No process was signalled.`);
|
|
15
|
+
return current.kind === "ours";
|
|
16
16
|
}
|
|
17
17
|
/** Signal `pid`; false when it has already exited. */
|
|
18
18
|
function signalService(pid, signal) {
|
|
@@ -33,12 +33,13 @@ async function signalIfStillRunning(directory, service, signal) {
|
|
|
33
33
|
const owner = await readServiceLeaseOwner(directory);
|
|
34
34
|
if (owner?.pid !== service.pid || owner.startedAt !== service.startedAt)
|
|
35
35
|
return false;
|
|
36
|
-
return (await isServiceProcess(owner)) &&
|
|
36
|
+
return ((await isServiceProcess(directory, owner)) &&
|
|
37
|
+
signalService(owner.pid, signal));
|
|
37
38
|
}
|
|
38
|
-
async function exitedWithin(service, ms) {
|
|
39
|
+
async function exitedWithin(directory, service, ms) {
|
|
39
40
|
const deadline = Date.now() + ms;
|
|
40
41
|
for (;;) {
|
|
41
|
-
if (!(await isServiceProcess(service)))
|
|
42
|
+
if (!(await isServiceProcess(directory, service)))
|
|
42
43
|
return true;
|
|
43
44
|
if (Date.now() >= deadline)
|
|
44
45
|
return false;
|
|
@@ -57,11 +58,11 @@ export async function terminatePrivateLocalService(directory, service) {
|
|
|
57
58
|
throw new Error("The Fungi service runs inside this process; stop it from within instead of with fungi stop.");
|
|
58
59
|
if (!(await signalIfStillRunning(directory, service, "SIGTERM")))
|
|
59
60
|
return "stopped";
|
|
60
|
-
if (await exitedWithin(service, terminationGraceMs))
|
|
61
|
+
if (await exitedWithin(directory, service, terminationGraceMs))
|
|
61
62
|
return "stopped";
|
|
62
63
|
if (!(await signalIfStillRunning(directory, service, "SIGKILL")))
|
|
63
64
|
return "stopped";
|
|
64
|
-
if (await exitedWithin(service, terminationGraceMs))
|
|
65
|
+
if (await exitedWithin(directory, service, terminationGraceMs))
|
|
65
66
|
return "killed";
|
|
66
67
|
throw new Error(`Local service process ${String(service.pid)} is still running after SIGKILL`);
|
|
67
68
|
}
|
|
@@ -4,6 +4,7 @@ import { readBoundedResponse } from "./private-bounded-response.js";
|
|
|
4
4
|
import { LocalSandboxUnavailable, sandboxUnavailableReasonSchema, } from "./private-local-containment.js";
|
|
5
5
|
export { LocalSandboxUnavailable } from "./private-local-containment.js";
|
|
6
6
|
import { capturePrivateLocalConfigRoot } from "./private-local-config-root.js";
|
|
7
|
+
import { takePendingExtensionArchiveNotices } from "./private-local-extension-archive-notice.js";
|
|
7
8
|
import { stopAllResultSchema } from "./private-local-stop-all.js";
|
|
8
9
|
import { LocalAgentSelectionAmbiguous, LocalSessionPathConflict, } from "./private-local-service-owner.js";
|
|
9
10
|
export { LocalAgentSelectionAmbiguous, LocalSessionPathConflict, } from "./private-local-service-owner.js";
|
|
@@ -111,6 +112,16 @@ export function createPrivateLocalServiceManager(options = {}) {
|
|
|
111
112
|
? capturePrivateLocalExecPath(process.env.PATH ?? "/usr/bin:/bin", process.cwd())
|
|
112
113
|
: parsePrivateLocalExecPath(options.executionPath);
|
|
113
114
|
const expectedVersion = privateLocalServiceVersion();
|
|
115
|
+
async function reportPendingExtensionArchiveNotices() {
|
|
116
|
+
try {
|
|
117
|
+
const notices = await takePendingExtensionArchiveNotices(root.home);
|
|
118
|
+
for (const notice of notices)
|
|
119
|
+
process.stderr.write(`${notice}\n`);
|
|
120
|
+
}
|
|
121
|
+
catch (cause) {
|
|
122
|
+
logEvent("extension.archive-notice", cause);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
114
125
|
/**
|
|
115
126
|
* Set once this client has reached a service. Only a starting client
|
|
116
127
|
* replaces a service speaking another protocol. Same-protocol build changes
|
|
@@ -344,6 +355,7 @@ export function createPrivateLocalServiceManager(options = {}) {
|
|
|
344
355
|
request.signal?.throwIfAborted();
|
|
345
356
|
const service = await start(request);
|
|
346
357
|
const selected = await postSelection(endpoint, input, service, request.signal);
|
|
358
|
+
await reportPendingExtensionArchiveNotices();
|
|
347
359
|
const firstBase = routeBase(service, selected);
|
|
348
360
|
/**
|
|
349
361
|
* Where this connection's client currently talks. A restarted service
|
|
@@ -17,6 +17,12 @@ export declare function unreadableStoreReason(agentId: string): string;
|
|
|
17
17
|
*/
|
|
18
18
|
export declare function isArchivedStore(path: string): boolean;
|
|
19
19
|
export declare function listPrivateLocalAgentStores(root: string, visibility?: LocalStoreVisibility): Promise<readonly string[]>;
|
|
20
|
+
/** Entry-preserving survey: a symlink is archived as a link, not its target. */
|
|
21
|
+
export declare function probePrivateLocalAgentStoreEntries(home: string, maximum?: number): Promise<readonly Readonly<{
|
|
22
|
+
agentId: string;
|
|
23
|
+
entry: string;
|
|
24
|
+
databasePath: string;
|
|
25
|
+
}>[]>;
|
|
20
26
|
/** A Local or Woodstock store outside the current schema; Local never migrates it. */
|
|
21
27
|
export declare class LocalStoreSchemaError extends Error {
|
|
22
28
|
readonly databasePath: string;
|
|
@@ -102,6 +102,37 @@ export async function listPrivateLocalAgentStores(root, visibility = "active") {
|
|
|
102
102
|
}
|
|
103
103
|
return [...paths];
|
|
104
104
|
}
|
|
105
|
+
/** Entry-preserving survey: a symlink is archived as a link, not its target. */
|
|
106
|
+
export async function probePrivateLocalAgentStoreEntries(home, maximum = 512) {
|
|
107
|
+
const directory = join(home, ".fungi", "agents");
|
|
108
|
+
let entries;
|
|
109
|
+
try {
|
|
110
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
111
|
+
}
|
|
112
|
+
catch (cause) {
|
|
113
|
+
if (cause instanceof Error && "code" in cause && cause.code === "ENOENT")
|
|
114
|
+
return [];
|
|
115
|
+
throw cause;
|
|
116
|
+
}
|
|
117
|
+
const stores = entries.filter((entry) => entry.isDirectory() || entry.isSymbolicLink());
|
|
118
|
+
if (stores.length > maximum)
|
|
119
|
+
throw new Error(`Fungi cannot survey ${String(stores.length)} Agent stores (maximum ${String(maximum)})`);
|
|
120
|
+
const candidates = [];
|
|
121
|
+
for (const store of stores) {
|
|
122
|
+
const entry = join(directory, store.name);
|
|
123
|
+
const databasePath = join(entry, "agent.sqlite");
|
|
124
|
+
try {
|
|
125
|
+
if ((await stat(databasePath)).isFile())
|
|
126
|
+
candidates.push({ agentId: store.name, entry, databasePath });
|
|
127
|
+
}
|
|
128
|
+
catch (cause) {
|
|
129
|
+
if (cause instanceof Error && "code" in cause && cause.code === "ENOENT")
|
|
130
|
+
continue;
|
|
131
|
+
throw cause;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return candidates;
|
|
135
|
+
}
|
|
105
136
|
const storedProfileSchema = z.strictObject({
|
|
106
137
|
name: z.string().nullable(),
|
|
107
138
|
createdBy: z.string().nullable(),
|
|
@@ -51,6 +51,7 @@ declare const answerResult: Schema.Union<readonly [Schema.Struct<{
|
|
|
51
51
|
readonly kind: Schema.Literal<"policy">;
|
|
52
52
|
}>]>>;
|
|
53
53
|
readonly reason: Schema.NullOr<Schema.Literals<readonly ["not-found", "not-eligible", "already-answered", "stale-revision", "expired", "declined", "fenced", "not-granted", "read-only", "pending", "in-flight", "outcome-unknown", "identity-conflict"]>>;
|
|
54
|
+
readonly denialNote: Schema.optionalKey<Schema.String>;
|
|
54
55
|
readonly requestedAt: Schema.Int;
|
|
55
56
|
readonly expiresAt: Schema.Int;
|
|
56
57
|
readonly updatedAt: Schema.Int;
|
|
@@ -62,6 +63,7 @@ declare const answerResult: Schema.Union<readonly [Schema.Struct<{
|
|
|
62
63
|
}>, Schema.Struct<{
|
|
63
64
|
readonly kind: Schema.Literal<"refused">;
|
|
64
65
|
readonly reason: Schema.Literals<readonly ["not-found", "not-eligible", "already-answered", "stale-revision", "expired", "declined", "fenced", "not-granted", "read-only", "pending", "in-flight", "outcome-unknown", "identity-conflict"]>;
|
|
66
|
+
readonly denialNote: Schema.optionalKey<Schema.String>;
|
|
65
67
|
}>, Schema.Struct<{
|
|
66
68
|
readonly kind: Schema.Literal<"submitted">;
|
|
67
69
|
}>, Schema.Struct<{
|
|
@@ -79,6 +81,7 @@ export declare const mcpFormApprovalContract: {
|
|
|
79
81
|
readonly id: Schema.String;
|
|
80
82
|
readonly decision: Schema.Literals<readonly ["approve", "deny", "always-allow"]>;
|
|
81
83
|
readonly expectedRevision: Schema.Int;
|
|
84
|
+
readonly denialNote: Schema.optionalKey<Schema.String>;
|
|
82
85
|
}>;
|
|
83
86
|
output: Schema.Union<readonly [Schema.Struct<{
|
|
84
87
|
readonly kind: Schema.Literal<"answered">;
|
|
@@ -115,6 +118,7 @@ export declare const mcpFormApprovalContract: {
|
|
|
115
118
|
readonly kind: Schema.Literal<"policy">;
|
|
116
119
|
}>]>>;
|
|
117
120
|
readonly reason: Schema.NullOr<Schema.Literals<readonly ["not-found", "not-eligible", "already-answered", "stale-revision", "expired", "declined", "fenced", "not-granted", "read-only", "pending", "in-flight", "outcome-unknown", "identity-conflict"]>>;
|
|
121
|
+
readonly denialNote: Schema.optionalKey<Schema.String>;
|
|
118
122
|
readonly requestedAt: Schema.Int;
|
|
119
123
|
readonly expiresAt: Schema.Int;
|
|
120
124
|
readonly updatedAt: Schema.Int;
|
|
@@ -126,6 +130,7 @@ export declare const mcpFormApprovalContract: {
|
|
|
126
130
|
}>, Schema.Struct<{
|
|
127
131
|
readonly kind: Schema.Literal<"refused">;
|
|
128
132
|
readonly reason: Schema.Literals<readonly ["not-found", "not-eligible", "already-answered", "stale-revision", "expired", "declined", "fenced", "not-granted", "read-only", "pending", "in-flight", "outcome-unknown", "identity-conflict"]>;
|
|
133
|
+
readonly denialNote: Schema.optionalKey<Schema.String>;
|
|
129
134
|
}>, Schema.Struct<{
|
|
130
135
|
readonly kind: Schema.Literal<"submitted">;
|
|
131
136
|
}>, Schema.Struct<{
|
|
@@ -171,6 +176,7 @@ export declare const mcpFormApprovalContract: {
|
|
|
171
176
|
readonly kind: Schema.Literal<"policy">;
|
|
172
177
|
}>]>>;
|
|
173
178
|
readonly reason: Schema.NullOr<Schema.Literals<readonly ["not-found", "not-eligible", "already-answered", "stale-revision", "expired", "declined", "fenced", "not-granted", "read-only", "pending", "in-flight", "outcome-unknown", "identity-conflict"]>>;
|
|
179
|
+
readonly denialNote: Schema.optionalKey<Schema.String>;
|
|
174
180
|
readonly requestedAt: Schema.Int;
|
|
175
181
|
readonly expiresAt: Schema.Int;
|
|
176
182
|
readonly updatedAt: Schema.Int;
|
|
@@ -183,10 +189,17 @@ export declare const mcpFormApprovalContract: {
|
|
|
183
189
|
readonly command: Schema.String;
|
|
184
190
|
readonly cwd: Schema.String;
|
|
185
191
|
readonly justification: Schema.String;
|
|
192
|
+
readonly form: Schema.Literals<readonly ["direct", "shell"]>;
|
|
193
|
+
readonly kind: Schema.Literals<readonly ["exec", "extension-install"]>;
|
|
194
|
+
readonly argv: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
186
195
|
readonly prefix: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
187
196
|
readonly warning: Schema.optionalKey<Schema.String>;
|
|
188
197
|
readonly alwaysAllowCoverage: Schema.optionalKey<Schema.String>;
|
|
189
198
|
readonly alwaysAllowUnavailableReason: Schema.optionalKey<Schema.String>;
|
|
199
|
+
readonly risk: Schema.optionalKey<Schema.Struct<{
|
|
200
|
+
readonly summary: Schema.String;
|
|
201
|
+
readonly detail: Schema.String;
|
|
202
|
+
}>>;
|
|
190
203
|
}>>;
|
|
191
204
|
}>>;
|
|
192
205
|
}>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ExtensionId, } from "@fungi.computer/shiitake/extensions";
|
|
2
2
|
import * as Schema from "effect/Schema";
|
|
3
|
-
import { ApprovalAnswerInputSchema, ApprovalRefusalSchema, ApprovalViewSchema, } from "@fungi.computer/approvals";
|
|
3
|
+
import { ApprovalAnswerInputSchema, ApprovalDenialNoteSchema, ApprovalRefusalSchema, ApprovalViewSchema, } from "@fungi.computer/approvals";
|
|
4
4
|
/** The actual MCP form, not a ledger record or an authorization decision. */
|
|
5
5
|
export const McpFormApprovalRequestSchema = Schema.Struct({
|
|
6
6
|
message: Schema.String,
|
|
@@ -36,10 +36,14 @@ const McpFormPendingSchema = Schema.Struct({
|
|
|
36
36
|
command: Schema.String,
|
|
37
37
|
cwd: Schema.String,
|
|
38
38
|
justification: Schema.String,
|
|
39
|
+
form: Schema.Literals(["direct", "shell"]),
|
|
40
|
+
kind: Schema.Literals(["exec", "extension-install"]),
|
|
41
|
+
argv: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
39
42
|
prefix: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
40
43
|
warning: Schema.optionalKey(Schema.String),
|
|
41
44
|
alwaysAllowCoverage: Schema.optionalKey(Schema.String),
|
|
42
45
|
alwaysAllowUnavailableReason: Schema.optionalKey(Schema.String),
|
|
46
|
+
risk: Schema.optionalKey(Schema.Struct({ summary: Schema.String, detail: Schema.String })),
|
|
43
47
|
})),
|
|
44
48
|
});
|
|
45
49
|
/** The existing Session extension slot carries only display-safe form data. */
|
|
@@ -52,6 +56,7 @@ export const mcpFormApprovalContract = {
|
|
|
52
56
|
id: Schema.String,
|
|
53
57
|
decision: Schema.Literals(["approve", "deny", "always-allow"]),
|
|
54
58
|
expectedRevision: ApprovalAnswerInputSchema.fields.expectedRevision,
|
|
59
|
+
denialNote: Schema.optionalKey(ApprovalDenialNoteSchema),
|
|
55
60
|
}),
|
|
56
61
|
output: answerResult,
|
|
57
62
|
},
|
|
@@ -5,10 +5,17 @@ export type LocalApprovalReview = {
|
|
|
5
5
|
command: string;
|
|
6
6
|
cwd: string;
|
|
7
7
|
justification: string;
|
|
8
|
+
form: "direct" | "shell";
|
|
9
|
+
kind: "exec" | "extension-install";
|
|
10
|
+
argv?: string[];
|
|
8
11
|
prefix?: readonly string[];
|
|
9
12
|
warning?: string;
|
|
10
13
|
alwaysAllowCoverage?: string;
|
|
11
14
|
alwaysAllowUnavailableReason?: string;
|
|
15
|
+
risk?: {
|
|
16
|
+
summary: string;
|
|
17
|
+
detail: string;
|
|
18
|
+
};
|
|
12
19
|
};
|
|
13
20
|
type Pending = {
|
|
14
21
|
sessionId: string;
|
|
@@ -27,6 +34,7 @@ export declare function createMcpFormSession(): {
|
|
|
27
34
|
readonly id: import("effect/Schema").String;
|
|
28
35
|
readonly decision: import("effect/Schema").Literals<readonly ["approve", "deny", "always-allow"]>;
|
|
29
36
|
readonly expectedRevision: import("effect/Schema").Int;
|
|
37
|
+
readonly denialNote: import("effect/Schema").optionalKey<import("effect/Schema").String>;
|
|
30
38
|
}>;
|
|
31
39
|
output: import("effect/Schema").Union<readonly [import("effect/Schema").Struct<{
|
|
32
40
|
readonly kind: import("effect/Schema").Literal<"answered">;
|
|
@@ -63,6 +71,7 @@ export declare function createMcpFormSession(): {
|
|
|
63
71
|
readonly kind: import("effect/Schema").Literal<"policy">;
|
|
64
72
|
}>]>>;
|
|
65
73
|
readonly reason: import("effect/Schema").NullOr<import("effect/Schema").Literals<readonly ["not-found", "not-eligible", "already-answered", "stale-revision", "expired", "declined", "fenced", "not-granted", "read-only", "pending", "in-flight", "outcome-unknown", "identity-conflict"]>>;
|
|
74
|
+
readonly denialNote: import("effect/Schema").optionalKey<import("effect/Schema").String>;
|
|
66
75
|
readonly requestedAt: import("effect/Schema").Int;
|
|
67
76
|
readonly expiresAt: import("effect/Schema").Int;
|
|
68
77
|
readonly updatedAt: import("effect/Schema").Int;
|
|
@@ -74,6 +83,7 @@ export declare function createMcpFormSession(): {
|
|
|
74
83
|
}>, import("effect/Schema").Struct<{
|
|
75
84
|
readonly kind: import("effect/Schema").Literal<"refused">;
|
|
76
85
|
readonly reason: import("effect/Schema").Literals<readonly ["not-found", "not-eligible", "already-answered", "stale-revision", "expired", "declined", "fenced", "not-granted", "read-only", "pending", "in-flight", "outcome-unknown", "identity-conflict"]>;
|
|
86
|
+
readonly denialNote: import("effect/Schema").optionalKey<import("effect/Schema").String>;
|
|
77
87
|
}>, import("effect/Schema").Struct<{
|
|
78
88
|
readonly kind: import("effect/Schema").Literal<"submitted">;
|
|
79
89
|
}>, import("effect/Schema").Struct<{
|
|
@@ -115,6 +125,7 @@ export declare function createMcpFormSession(): {
|
|
|
115
125
|
readonly expiresAt: number;
|
|
116
126
|
readonly updatedAt: number;
|
|
117
127
|
readonly revision: number;
|
|
128
|
+
readonly denialNote?: string;
|
|
118
129
|
readonly evidence?: readonly {
|
|
119
130
|
readonly kind: "artifact";
|
|
120
131
|
readonly ref: string;
|
|
@@ -126,13 +137,20 @@ export declare function createMcpFormSession(): {
|
|
|
126
137
|
readonly canSetAutoApprove: boolean;
|
|
127
138
|
};
|
|
128
139
|
readonly review?: {
|
|
140
|
+
readonly kind: "exec" | "extension-install";
|
|
141
|
+
readonly form: "direct" | "shell";
|
|
129
142
|
readonly command: string;
|
|
130
143
|
readonly cwd: string;
|
|
131
144
|
readonly justification: string;
|
|
145
|
+
readonly argv?: readonly string[];
|
|
132
146
|
readonly prefix?: readonly string[];
|
|
133
147
|
readonly warning?: string;
|
|
134
148
|
readonly alwaysAllowCoverage?: string;
|
|
135
149
|
readonly alwaysAllowUnavailableReason?: string;
|
|
150
|
+
readonly risk?: {
|
|
151
|
+
readonly summary: string;
|
|
152
|
+
readonly detail: string;
|
|
153
|
+
};
|
|
136
154
|
};
|
|
137
155
|
}[];
|
|
138
156
|
}, {
|
|
@@ -169,6 +187,7 @@ export declare function createMcpFormSession(): {
|
|
|
169
187
|
readonly expiresAt: number;
|
|
170
188
|
readonly updatedAt: number;
|
|
171
189
|
readonly revision: number;
|
|
190
|
+
readonly denialNote?: string;
|
|
172
191
|
readonly evidence?: readonly {
|
|
173
192
|
readonly kind: "artifact";
|
|
174
193
|
readonly ref: string;
|
|
@@ -180,13 +199,20 @@ export declare function createMcpFormSession(): {
|
|
|
180
199
|
readonly canSetAutoApprove: boolean;
|
|
181
200
|
};
|
|
182
201
|
readonly review?: {
|
|
202
|
+
readonly kind: "exec" | "extension-install";
|
|
203
|
+
readonly form: "direct" | "shell";
|
|
183
204
|
readonly command: string;
|
|
184
205
|
readonly cwd: string;
|
|
185
206
|
readonly justification: string;
|
|
207
|
+
readonly argv?: readonly string[];
|
|
186
208
|
readonly prefix?: readonly string[];
|
|
187
209
|
readonly warning?: string;
|
|
188
210
|
readonly alwaysAllowCoverage?: string;
|
|
189
211
|
readonly alwaysAllowUnavailableReason?: string;
|
|
212
|
+
readonly risk?: {
|
|
213
|
+
readonly summary: string;
|
|
214
|
+
readonly detail: string;
|
|
215
|
+
};
|
|
190
216
|
};
|
|
191
217
|
}[];
|
|
192
218
|
}>;
|
|
@@ -49,15 +49,18 @@ export function createMcpFormSession() {
|
|
|
49
49
|
if (input.decision === "always-allow" &&
|
|
50
50
|
entry.alwaysAllow === undefined)
|
|
51
51
|
return { kind: "refused", reason: "not-pending" };
|
|
52
|
+
if (input.denialNote !== undefined && input.decision !== "deny")
|
|
53
|
+
return { kind: "refused", reason: "not-pending" };
|
|
52
54
|
if (input.decision === "always-allow")
|
|
53
55
|
entry.alwaysAllow?.("prefix");
|
|
54
|
-
const
|
|
56
|
+
const answer = {
|
|
55
57
|
recordId: entry.approval.record.recordId,
|
|
56
|
-
decision: input.decision === "
|
|
57
|
-
? "approve"
|
|
58
|
-
: input.decision,
|
|
58
|
+
decision: input.decision === "deny" ? "deny" : "approve",
|
|
59
59
|
expectedRevision: input.expectedRevision,
|
|
60
|
-
}
|
|
60
|
+
};
|
|
61
|
+
const result = await entry.approvals.answer(input.denialNote === undefined
|
|
62
|
+
? answer
|
|
63
|
+
: { ...answer, denialNote: input.denialNote });
|
|
61
64
|
return result;
|
|
62
65
|
}
|
|
63
66
|
finally {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fungi.computer/truffle",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Truffle: Fungi's local Agent runtime, bundled into the fungi command",
|
|
6
6
|
"type": "module",
|
|
@@ -50,6 +50,10 @@
|
|
|
50
50
|
"types": "./dist/local-extensions.d.ts",
|
|
51
51
|
"import": "./dist/local-extensions.js"
|
|
52
52
|
},
|
|
53
|
+
"./upgrade": {
|
|
54
|
+
"types": "./dist/local-upgrade.d.ts",
|
|
55
|
+
"import": "./dist/local-upgrade.js"
|
|
56
|
+
},
|
|
53
57
|
"./rules": {
|
|
54
58
|
"types": "./dist/private-local-exec-rules.d.ts",
|
|
55
59
|
"import": "./dist/private-local-exec-rules.js"
|
|
@@ -65,26 +69,26 @@
|
|
|
65
69
|
"@cf-wasm/photon": "0.4.0",
|
|
66
70
|
"@earendil-works/pi-ai": "1.3.0",
|
|
67
71
|
"@effect/platform-node": "4.0.2",
|
|
68
|
-
"@fungi.computer/agar": "0.
|
|
69
|
-
"@fungi.computer/approvals": "0.
|
|
70
|
-
"@fungi.computer/bird-dog": "0.2.
|
|
72
|
+
"@fungi.computer/agar": "0.4.0",
|
|
73
|
+
"@fungi.computer/approvals": "0.2.0",
|
|
74
|
+
"@fungi.computer/bird-dog": "0.2.1",
|
|
71
75
|
"@fungi.computer/cairn": "0.1.0",
|
|
72
76
|
"@fungi.computer/capnweb": "0.12.0",
|
|
73
77
|
"@fungi.computer/carrier": "0.2.0",
|
|
74
78
|
"@fungi.computer/cordyceps": "0.4.0",
|
|
75
79
|
"@fungi.computer/gills": "0.2.0",
|
|
76
|
-
"@fungi.computer/graft": "0.1.
|
|
77
|
-
"@fungi.computer/knapsack": "0.3.
|
|
80
|
+
"@fungi.computer/graft": "0.1.3",
|
|
81
|
+
"@fungi.computer/knapsack": "0.3.1",
|
|
78
82
|
"@fungi.computer/lantern": "0.3.0",
|
|
79
83
|
"@fungi.computer/mycelium": "0.2.2",
|
|
80
|
-
"@fungi.computer/nacre": "0.0.
|
|
84
|
+
"@fungi.computer/nacre": "0.0.3",
|
|
81
85
|
"@fungi.computer/seam": "0.2.0",
|
|
82
|
-
"@fungi.computer/shiitake": "0.
|
|
83
|
-
"@fungi.computer/shiitake-extensions": "0.
|
|
84
|
-
"@fungi.computer/veil": "0.2.
|
|
86
|
+
"@fungi.computer/shiitake": "0.5.0",
|
|
87
|
+
"@fungi.computer/shiitake-extensions": "0.3.0",
|
|
88
|
+
"@fungi.computer/veil": "0.2.1",
|
|
85
89
|
"@fungi.computer/watchdog": "0.1.0",
|
|
86
|
-
"@fungi.computer/whistle": "0.
|
|
87
|
-
"@fungi.computer/zookeeper": "0.0.
|
|
90
|
+
"@fungi.computer/whistle": "0.2.0",
|
|
91
|
+
"@fungi.computer/zookeeper": "0.0.4",
|
|
88
92
|
"@jellybrick/dbus-next": "0.11.4",
|
|
89
93
|
"@modelcontextprotocol/client": "2.0.0",
|
|
90
94
|
"@napi-rs/keyring": "2.1.2",
|
|
@@ -97,6 +101,8 @@
|
|
|
97
101
|
"miniflare": "5.20260828.0-alpha",
|
|
98
102
|
"proper-lockfile": "4.1.2",
|
|
99
103
|
"re2js": "1.3.3",
|
|
104
|
+
"semver": "7.8.5",
|
|
105
|
+
"shell-quote": "1.10.0",
|
|
100
106
|
"write-file-atomic": "8.0.0",
|
|
101
107
|
"yaml": "2.9.0",
|
|
102
108
|
"zod": "4.4.3"
|
|
@@ -107,6 +113,7 @@
|
|
|
107
113
|
"@modelcontextprotocol/server": "2.0.0",
|
|
108
114
|
"@types/node": "24.13.3",
|
|
109
115
|
"@types/proper-lockfile": "4.1.4",
|
|
116
|
+
"@types/semver": "7.8.0",
|
|
110
117
|
"@types/write-file-atomic": "4.0.3",
|
|
111
118
|
"bun": "1.4.2",
|
|
112
119
|
"bun-types": "1.4.1",
|
|
@@ -154,7 +161,7 @@
|
|
|
154
161
|
"test:node": "../../node_modules/.bin/vitest run",
|
|
155
162
|
"test:bun": "pnpm run test:mesh-link && pnpm run test:bun:batch && bun test ./test/private-workerd-deadline.bun.test.ts",
|
|
156
163
|
"test:mesh-link": "bun test ./test/private-local-mesh-links.bun.test.ts",
|
|
157
|
-
"test:bun:batch": "bun test ./test/private-sqlite-approval-driver.bun.test.ts ./test/mcp-form-service.bun.test.ts ./test/private-local-service-discovery.bun.test.ts ./test/private-local-service-listener.bun.test.ts ./test/private-local-service-manager.bun.test.ts ./test/private-local-service-rebind.bun.test.ts ./test/private-local-service-stop.bun.test.ts ./test/private-local-service-ensure.bun.test.ts ./test/private-local-service-identity.bun.test.ts ./test/private-local-service-kill-resume.bun.test.ts ./test/private-local-service-idle-owners.bun.test.ts ./test/private-local-service-idle-delegation.bun.test.ts ./test/private-local-mail-retry.bun.test.ts ./test/private-local-service-log-native.bun.test.ts ./test/private-local-service-log.bun.test.ts ./test/private-local-sqlite-lifetime.bun.test.ts ./test/private-sqlite-driver.bun.test.ts ./test/private-local-reload.bun.test.ts ./test/private-local-auth-rpc.bun.test.ts ./test/private-workerd-forage.bun.test.ts ./test/local-extensions.bun.test.ts ./test/local-zookeeper-slot.bun.test.ts ./test/local-extension-skills.bun.test.ts ./test/private-local-sandbox-service.bun.test.ts ./test/private-local-native-service.bun.test.ts",
|
|
164
|
+
"test:bun:batch": "bun test ./test/private-sqlite-approval-driver.bun.test.ts ./test/mcp-form-service.bun.test.ts ./test/private-local-service-discovery.bun.test.ts ./test/private-local-service-listener.bun.test.ts ./test/private-local-service-manager.bun.test.ts ./test/private-local-service-rebind.bun.test.ts ./test/private-local-service-stop.bun.test.ts ./test/private-local-service-ensure.bun.test.ts ./test/private-local-service-identity.bun.test.ts ./test/private-local-service-kill-resume.bun.test.ts ./test/private-local-service-idle-owners.bun.test.ts ./test/private-local-service-idle-delegation.bun.test.ts ./test/private-local-mail-retry.bun.test.ts ./test/private-local-service-log-native.bun.test.ts ./test/private-local-service-log.bun.test.ts ./test/private-local-sqlite-lifetime.bun.test.ts ./test/private-sqlite-driver.bun.test.ts ./test/private-local-reload.bun.test.ts ./test/private-local-auth-rpc.bun.test.ts ./test/private-workerd-forage.bun.test.ts ./test/local-extensions.bun.test.ts ./test/local-extension-archive.bun.test.ts ./test/local-zookeeper-slot.bun.test.ts ./test/local-extension-skills.bun.test.ts ./test/private-local-sandbox-service.bun.test.ts ./test/private-local-native-service.bun.test.ts",
|
|
158
165
|
"build:native": "fungi-apply-patch-build --out native/bin && node scripts/build-bwrap.mjs --out native/bin && node scripts/generate-native-digests.mjs",
|
|
159
166
|
"test:patch": "node --test --test-timeout=30000 test/apply-patch.native.mjs",
|
|
160
167
|
"test:sandbox:linux": "node --import tsx test/fixtures/local-sandbox-proof.mts",
|