@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
|
@@ -6,8 +6,9 @@ import { logEvent } from "./log-event.js";
|
|
|
6
6
|
import { localArchiveHandoff } from "./local-archive-handoff.js";
|
|
7
7
|
import { createMcpFormSession } from "./private-mcp-form-session.js";
|
|
8
8
|
import { localExecEscapeReview } from "./private-local-exec-review.js";
|
|
9
|
-
import { ApprovalLedger, } from "@fungi.computer/approvals";
|
|
9
|
+
import { ApprovalLedger, denialFeedbackOpening, } from "@fungi.computer/approvals";
|
|
10
10
|
import { localExtensions, } from "./local-extensions.js";
|
|
11
|
+
import { deferExtensionArchiveNotices } from "./private-local-extension-archive-notice.js";
|
|
11
12
|
import { lstat, mkdir, realpath, stat } from "node:fs/promises";
|
|
12
13
|
import { lstatSync, mkdirSync, realpathSync } from "node:fs";
|
|
13
14
|
import { basename, dirname, isAbsolute, join, relative, resolve, } from "node:path";
|
|
@@ -19,7 +20,7 @@ import { parseModelPreference, } from "@fungi.computer/shiitake/model-preference
|
|
|
19
20
|
import { AgentCreateRefused, createAgentControlSource, } from "@fungi.computer/shiitake/agent-control";
|
|
20
21
|
import { defineIdFamily } from "@fungi.computer/cairn";
|
|
21
22
|
import { words } from "@fungi.computer/cairn/words";
|
|
22
|
-
import { botanicalExtensionsWithSkills } from "@fungi.computer/shiitake-extensions/roster";
|
|
23
|
+
import { botanicalExtensionsWithSkills, botanicalExtensionsWithSubagents, } from "@fungi.computer/shiitake-extensions/roster";
|
|
23
24
|
import { homeExtension, homeGrant, } from "@fungi.computer/shiitake-extensions/home/host";
|
|
24
25
|
import { Knapsack } from "@fungi.computer/knapsack";
|
|
25
26
|
import { makeHost, Shiitake, ShiitakeError, } from "@fungi.computer/shiitake";
|
|
@@ -45,11 +46,11 @@ import { createPrivateInvocationFilesPort, } from "./private-invocation-files.js
|
|
|
45
46
|
import { createLocalSkillSource } from "./local-skill-source.js";
|
|
46
47
|
import { createPrivateWorkerdSandbox } from "./private-workerd-sandbox.js";
|
|
47
48
|
import { createPrivateFungiMcpConnection, } from "./private-fungi-mcp-connection.js";
|
|
48
|
-
import { createPrivateSqliteOwner, openPrivateWritableSqliteDatabase, } from "./private-sqlite-owner.js";
|
|
49
|
+
import { createPrivateSqliteOwner, openPrivateWritableSqliteDatabase, storeWriteBusyTimeoutMs, } from "./private-sqlite-owner.js";
|
|
49
50
|
import { createPrivateLocalMailRoom, hasPrivateLocalPendingMail, } from "./private-local-mail.js";
|
|
50
51
|
import { createPrivateLocalDelegation, } from "./private-local-delegation.js";
|
|
51
52
|
import { createPrivateLocalAgentControl, } from "./private-local-agent-control.js";
|
|
52
|
-
import {
|
|
53
|
+
import {} from "@fungi.computer/shiitake-extensions/subagents/host";
|
|
53
54
|
const initialSessionId = "main";
|
|
54
55
|
const maximumInstructionReadBytes = 128 * 1_024;
|
|
55
56
|
const maximumInstructionSnapshotBytes = 32_000;
|
|
@@ -206,10 +207,40 @@ async function assertPrivateLocalReceiptFile(path, kind) {
|
|
|
206
207
|
throw cause;
|
|
207
208
|
}
|
|
208
209
|
}
|
|
210
|
+
function ensureReceiptTable(database) {
|
|
211
|
+
database.exec(`
|
|
212
|
+
CREATE TABLE IF NOT EXISTS ${localCreateReceiptTable} (
|
|
213
|
+
request_id TEXT PRIMARY KEY NOT NULL,
|
|
214
|
+
agent_id TEXT NOT NULL UNIQUE,
|
|
215
|
+
name TEXT,
|
|
216
|
+
created_by_agent_id TEXT,
|
|
217
|
+
created_by_session_id TEXT
|
|
218
|
+
) WITHOUT ROWID
|
|
219
|
+
`);
|
|
220
|
+
const receiptColumns = database
|
|
221
|
+
.prepare(`PRAGMA table_info(${localCreateReceiptTable})`)
|
|
222
|
+
.all()
|
|
223
|
+
.map((column) => column.name);
|
|
224
|
+
if (!receiptColumns.includes("created_by_agent_id"))
|
|
225
|
+
database.exec(`ALTER TABLE ${localCreateReceiptTable} ADD COLUMN created_by_agent_id TEXT`);
|
|
226
|
+
if (!receiptColumns.includes("created_by_session_id"))
|
|
227
|
+
database.exec(`ALTER TABLE ${localCreateReceiptTable} ADD COLUMN created_by_session_id TEXT`);
|
|
228
|
+
}
|
|
229
|
+
function readReceiptRow(database, requestId) {
|
|
230
|
+
return database
|
|
231
|
+
.prepare(`SELECT agent_id, created_by_agent_id, created_by_session_id FROM ${localCreateReceiptTable} WHERE request_id = ?`)
|
|
232
|
+
.get(requestId);
|
|
233
|
+
}
|
|
209
234
|
/** Resolve an Agent create request through its durable Local receipt. */
|
|
210
|
-
export async function resolvePrivateLocalCreateStore(root, requestId) {
|
|
235
|
+
export async function resolvePrivateLocalCreateStore(root, requestId, creator) {
|
|
211
236
|
if (requestId.length === 0 || requestId.length > 256)
|
|
212
237
|
throw new Error("Local Agent create request id is invalid");
|
|
238
|
+
const creatorAgentId = creator === undefined
|
|
239
|
+
? undefined
|
|
240
|
+
: z.string().min(1).max(256).parse(creator.agentId);
|
|
241
|
+
const creatorSessionId = creator === undefined
|
|
242
|
+
? undefined
|
|
243
|
+
: z.string().min(1).max(256).parse(creator.sessionId);
|
|
213
244
|
await ensurePrivateLocalGitExclude(root);
|
|
214
245
|
const fungiDirectory = join(root, ".fungi");
|
|
215
246
|
const agentsDirectory = join(fungiDirectory, "agents");
|
|
@@ -222,17 +253,17 @@ export async function resolvePrivateLocalCreateStore(root, requestId) {
|
|
|
222
253
|
try {
|
|
223
254
|
database.exec(`PRAGMA busy_timeout = ${String(receiptBusyTimeoutMs)}`);
|
|
224
255
|
database.exec("BEGIN IMMEDIATE");
|
|
225
|
-
database
|
|
226
|
-
|
|
227
|
-
request_id TEXT PRIMARY KEY NOT NULL,
|
|
228
|
-
agent_id TEXT NOT NULL UNIQUE,
|
|
229
|
-
name TEXT
|
|
230
|
-
) WITHOUT ROWID
|
|
231
|
-
`);
|
|
232
|
-
const existing = database
|
|
233
|
-
.prepare(`SELECT agent_id FROM ${localCreateReceiptTable} WHERE request_id = ?`)
|
|
234
|
-
.get(requestId);
|
|
256
|
+
ensureReceiptTable(database);
|
|
257
|
+
const existing = readReceiptRow(database, requestId);
|
|
235
258
|
if (existing !== undefined) {
|
|
259
|
+
if (creatorAgentId !== undefined &&
|
|
260
|
+
(existing.created_by_agent_id === null ||
|
|
261
|
+
existing.created_by_agent_id === undefined))
|
|
262
|
+
throw new AgentCreateRefused("request_conflict", "the Local Agent create receipt is unbound; use a fresh requestId");
|
|
263
|
+
if (creatorAgentId !== undefined &&
|
|
264
|
+
(existing.created_by_agent_id !== creatorAgentId ||
|
|
265
|
+
existing.created_by_session_id !== creatorSessionId))
|
|
266
|
+
throw new AgentCreateRefused("request_conflict", "the create request id is already bound to a different creating Agent or Session");
|
|
236
267
|
const parsed = localHostIds.store.schema.safeParse(existing.agent_id);
|
|
237
268
|
if (!parsed.success)
|
|
238
269
|
throw new Error("Local Agent create receipt is invalid");
|
|
@@ -245,7 +276,7 @@ export async function resolvePrivateLocalCreateStore(root, requestId) {
|
|
|
245
276
|
};
|
|
246
277
|
}
|
|
247
278
|
let agentId;
|
|
248
|
-
const insert = database.prepare(`INSERT OR IGNORE INTO ${localCreateReceiptTable} (request_id, agent_id) VALUES (?, ?)`);
|
|
279
|
+
const insert = database.prepare(`INSERT OR IGNORE INTO ${localCreateReceiptTable} (request_id, agent_id, created_by_agent_id, created_by_session_id) VALUES (?, ?, ?, ?)`);
|
|
249
280
|
for (let attempt = 0; attempt < maximumReceiptIdAttempts; attempt += 1) {
|
|
250
281
|
const candidate = localHostIds.store.generate();
|
|
251
282
|
const candidateDirectory = join(agentsDirectory, candidate);
|
|
@@ -267,7 +298,7 @@ export async function resolvePrivateLocalCreateStore(root, requestId) {
|
|
|
267
298
|
metadata.isSymbolicLink() ||
|
|
268
299
|
realpathSync(candidateDirectory) !== candidateDirectory)
|
|
269
300
|
throw new Error("Local Agent create directory is not project owned");
|
|
270
|
-
if (insert.run(requestId, candidate).changes > 0) {
|
|
301
|
+
if (insert.run(requestId, candidate, creatorAgentId ?? null, creatorSessionId ?? null).changes > 0) {
|
|
271
302
|
agentId = candidate;
|
|
272
303
|
break;
|
|
273
304
|
}
|
|
@@ -295,7 +326,7 @@ export async function resolvePrivateLocalCreateStore(root, requestId) {
|
|
|
295
326
|
database.close();
|
|
296
327
|
}
|
|
297
328
|
}
|
|
298
|
-
async function localDatabasePaths(root, requested, createRequestId) {
|
|
329
|
+
async function localDatabasePaths(root, requested, createRequestId, creator) {
|
|
299
330
|
if (requested !== undefined && createRequestId !== undefined)
|
|
300
331
|
throw new Error("Local Agent database and create request are mutually exclusive");
|
|
301
332
|
if (requested !== undefined) {
|
|
@@ -311,7 +342,7 @@ async function localDatabasePaths(root, requested, createRequestId) {
|
|
|
311
342
|
}
|
|
312
343
|
const createStore = createRequestId === undefined
|
|
313
344
|
? undefined
|
|
314
|
-
: await resolvePrivateLocalCreateStore(root, createRequestId);
|
|
345
|
+
: await resolvePrivateLocalCreateStore(root, createRequestId, creator);
|
|
315
346
|
const createDatabasePath = createStore?.databasePath;
|
|
316
347
|
const generatedAgentId = createStore?.agentId ?? localHostIds.store.generate();
|
|
317
348
|
const selectedParent = createDatabasePath === undefined
|
|
@@ -595,6 +626,13 @@ export function createPrivateLocalModelSource(models, providerId, modelId, provi
|
|
|
595
626
|
message.stopReason === "error" &&
|
|
596
627
|
modelFailure?.message === message &&
|
|
597
628
|
modelFailure.retryable,
|
|
629
|
+
retryAfterMs: (message) => modelFailure?.message === message &&
|
|
630
|
+
modelFailure.rejection?.code === "rate-limit"
|
|
631
|
+
? modelFailure.rejection.retryAfterMs
|
|
632
|
+
: undefined,
|
|
633
|
+
rejection: (message) => modelFailure?.message === message
|
|
634
|
+
? modelFailure.rejection
|
|
635
|
+
: undefined,
|
|
598
636
|
failureReason: (message) => modelFailure?.message === message ? modelFailure.reason : undefined,
|
|
599
637
|
providerError: (message) => modelFailure?.message === message
|
|
600
638
|
? modelFailure.providerError
|
|
@@ -616,7 +654,7 @@ export function createPrivateLocalModelSource(models, providerId, modelId, provi
|
|
|
616
654
|
const evidence = {
|
|
617
655
|
refusal: turn.refusal(),
|
|
618
656
|
upstream: upstream ?? partial,
|
|
619
|
-
rejection: provider.rejection(),
|
|
657
|
+
rejection: provider.rejection(upstream),
|
|
620
658
|
};
|
|
621
659
|
const message = cause === undefined
|
|
622
660
|
? modelFailureMessage(selected.provider, evidence)
|
|
@@ -653,6 +691,8 @@ export function createPrivateLocalModelSource(models, providerId, modelId, provi
|
|
|
653
691
|
reason: message,
|
|
654
692
|
retryable: modelFailureRetryable(evidence),
|
|
655
693
|
};
|
|
694
|
+
if (evidence.rejection !== undefined)
|
|
695
|
+
modelFailure.rejection = evidence.rejection;
|
|
656
696
|
if (upstream?.errorMessage !== undefined)
|
|
657
697
|
modelFailure.providerError = upstream.errorMessage;
|
|
658
698
|
}
|
|
@@ -778,6 +818,7 @@ subagents, configuration, configRoot = capturePrivateLocalConfigRoot(), sandbox,
|
|
|
778
818
|
: [
|
|
779
819
|
createPrivateLocalComputerSource(root, port, sessionPath, processes, configRoot.home, sandbox?.launch, currentMode, escapeForSession, policyForSession),
|
|
780
820
|
];
|
|
821
|
+
deferExtensionArchiveNotices(configRoot.home);
|
|
781
822
|
const installed = configuration?.installed ??
|
|
782
823
|
(storeDirectory === undefined
|
|
783
824
|
? { roster: [], grants: [], skills: [], diagnostics: [] }
|
|
@@ -793,9 +834,12 @@ subagents, configuration, configRoot = capturePrivateLocalConfigRoot(), sandbox,
|
|
|
793
834
|
const archive = storeDirectory === undefined
|
|
794
835
|
? { roster: [], grants: [] }
|
|
795
836
|
: localArchiveHandoff(storeDirectory, installed.grants);
|
|
837
|
+
const composed = subagents === undefined
|
|
838
|
+
? base
|
|
839
|
+
: botanicalExtensionsWithSubagents(base, subagents.source);
|
|
796
840
|
const extensions = {
|
|
797
841
|
roster: [
|
|
798
|
-
...
|
|
842
|
+
...composed.roster,
|
|
799
843
|
forms.extension,
|
|
800
844
|
homeExtension({
|
|
801
845
|
computers: {
|
|
@@ -808,20 +852,16 @@ subagents, configuration, configRoot = capturePrivateLocalConfigRoot(), sandbox,
|
|
|
808
852
|
],
|
|
809
853
|
},
|
|
810
854
|
}),
|
|
811
|
-
...(subagents === undefined
|
|
812
|
-
? []
|
|
813
|
-
: [subagentsExtension({ source: subagents.source })]),
|
|
814
855
|
...archive.roster,
|
|
815
856
|
...installed.roster,
|
|
816
857
|
],
|
|
817
858
|
grants: [
|
|
818
|
-
...
|
|
859
|
+
...composed.grants,
|
|
819
860
|
{
|
|
820
861
|
id: forms.extension.manifest.id,
|
|
821
862
|
capabilities: forms.extension.manifest.requests,
|
|
822
863
|
},
|
|
823
864
|
homeGrant,
|
|
824
|
-
...(subagents === undefined ? [] : [subagentsGrant]),
|
|
825
865
|
...archive.grants,
|
|
826
866
|
...installed.grants,
|
|
827
867
|
],
|
|
@@ -1002,7 +1042,12 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
|
|
|
1002
1042
|
if (reason !== undefined)
|
|
1003
1043
|
throw new LocalSandboxUnavailable(reason);
|
|
1004
1044
|
}
|
|
1005
|
-
const paths = await localDatabasePaths(root, input.databasePath, input.createRequestId
|
|
1045
|
+
const paths = await localDatabasePaths(root, input.databasePath, input.createRequestId, input.createdBy === undefined || input.createdBySessionId === undefined
|
|
1046
|
+
? undefined
|
|
1047
|
+
: {
|
|
1048
|
+
agentId: input.createdBy,
|
|
1049
|
+
sessionId: input.createdBySessionId,
|
|
1050
|
+
});
|
|
1006
1051
|
const runtime = localRuntimeOwner();
|
|
1007
1052
|
let lease;
|
|
1008
1053
|
let providerCacheKey;
|
|
@@ -1018,7 +1063,7 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
|
|
|
1018
1063
|
providerCacheKey = createHash("sha256")
|
|
1019
1064
|
.update(JSON.stringify(["fungi-local", paths.databasePath, initialSessionId]))
|
|
1020
1065
|
.digest("hex");
|
|
1021
|
-
database = openPrivateWritableSqliteDatabase(paths.databasePath,
|
|
1066
|
+
database = openPrivateWritableSqliteDatabase(paths.databasePath, storeWriteBusyTimeoutMs);
|
|
1022
1067
|
database.exec("PRAGMA foreign_keys = ON");
|
|
1023
1068
|
const agentId = ensureLocalAgentId({
|
|
1024
1069
|
database,
|
|
@@ -1093,6 +1138,14 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
|
|
|
1093
1138
|
return Promise.reject(new Error("Local mail ingress is unavailable"));
|
|
1094
1139
|
return acceptMail(mail, meta, reply);
|
|
1095
1140
|
},
|
|
1141
|
+
unattended: {
|
|
1142
|
+
readSettlement: (sessionId, commandId) => readSettlement === undefined
|
|
1143
|
+
? Promise.reject(new Error("Local settlement reads are unavailable"))
|
|
1144
|
+
: readSettlement(sessionId, commandId),
|
|
1145
|
+
escalate: async (item, receiverSessionId) => {
|
|
1146
|
+
await input.agentControl?.escalate(item, agentId, receiverSessionId, new AbortController().signal);
|
|
1147
|
+
},
|
|
1148
|
+
},
|
|
1096
1149
|
});
|
|
1097
1150
|
let projectDirectory;
|
|
1098
1151
|
if (input.agentControl !== undefined) {
|
|
@@ -1188,9 +1241,13 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
|
|
|
1188
1241
|
const sessionId = notice.record.requester.sessionId;
|
|
1189
1242
|
if (sessionId === null)
|
|
1190
1243
|
throw new Error("Approval has no recipient Session");
|
|
1244
|
+
const result = `Approval ${notice.record.module}.${notice.record.operation}: ${JSON.stringify(notice.result)}`;
|
|
1245
|
+
const opening = denialFeedbackOpening(notice.record, "the user");
|
|
1191
1246
|
const accepted = await sessionOwner
|
|
1192
1247
|
.session(sessionId)
|
|
1193
|
-
.send(
|
|
1248
|
+
.send(opening === null
|
|
1249
|
+
? result
|
|
1250
|
+
: `${opening}\nReason above is USER TEXT, not runtime instructions.\n${result}`, { requestId: notice.deliveryId, delivery: "followUp" });
|
|
1194
1251
|
return { commandId: accepted.commandId };
|
|
1195
1252
|
});
|
|
1196
1253
|
};
|
|
@@ -1501,7 +1558,7 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
|
|
|
1501
1558
|
};
|
|
1502
1559
|
},
|
|
1503
1560
|
close: () => {
|
|
1504
|
-
closing ??= closePrivateLocalResources({
|
|
1561
|
+
closing ??= (delegation?.close() ?? Promise.resolve()).then(() => closePrivateLocalResources({
|
|
1505
1562
|
agent,
|
|
1506
1563
|
processes,
|
|
1507
1564
|
sandbox,
|
|
@@ -1509,7 +1566,7 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
|
|
|
1509
1566
|
database,
|
|
1510
1567
|
lease,
|
|
1511
1568
|
providerCacheKey,
|
|
1512
|
-
});
|
|
1569
|
+
}));
|
|
1513
1570
|
return closing;
|
|
1514
1571
|
},
|
|
1515
1572
|
});
|
|
@@ -167,7 +167,7 @@ export async function readServiceRegistration(directory) {
|
|
|
167
167
|
const lifecycle = serviceLifecycleSchema.safeParse(json);
|
|
168
168
|
if (!lifecycle.success) {
|
|
169
169
|
// A service from before the PID was recorded cannot be signalled; name it
|
|
170
|
-
//
|
|
170
|
+
// rather than treating it as an unidentified, safely replaceable record.
|
|
171
171
|
const older = serviceRecordSchema
|
|
172
172
|
.pick({ serviceId: true, baseUrl: true })
|
|
173
173
|
.safeParse(json);
|
|
@@ -183,8 +183,7 @@ export async function readServiceRegistration(directory) {
|
|
|
183
183
|
class LocalServiceRecordWithoutPid extends Error {
|
|
184
184
|
name = "LocalServiceRecordWithoutPid";
|
|
185
185
|
constructor(directory, service) {
|
|
186
|
-
|
|
187
|
-
super(`An older Fungi service (${service.serviceId} on port ${new URL(service.baseUrl).port}) is registered in ${recordPath(directory)}, and this fungi cannot stop it. Stop it with \`curl -X POST ${stopUrl.href}\`; if it keeps running, end the process whose command line contains \`--internal-service ${directory}\`. If it is no longer running, delete ${recordPath(directory)}. Then retry; saved Sessions are kept.`);
|
|
186
|
+
super(`An older Fungi service (${service.serviceId} on port ${new URL(service.baseUrl).port}) is registered in ${recordPath(directory)} without a process identity. Fungi cannot safely signal it. End the process whose command line contains \`--internal-service ${directory}\`. If it is no longer running, move ${recordPath(directory)} aside. Then retry; saved Sessions are kept.`);
|
|
188
187
|
}
|
|
189
188
|
}
|
|
190
189
|
/** The record of a service speaking this client's protocol, if one is registered. */
|
|
@@ -10,18 +10,20 @@ 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";
|
|
16
17
|
import * as Schema from "effect/Schema";
|
|
17
18
|
import { selectionSchema } from "./private-local-service-discovery.js";
|
|
18
19
|
import { createShiitakeHttpRoutes } from "@fungi.computer/shiitake/http";
|
|
20
|
+
import { questionContract } from "@fungi.computer/shiitake-extensions/question";
|
|
19
21
|
import { listPrivateLocalAgentStores, LocalStoreSchemaError, readPrivateLocalAgentProfile, readPrivateLocalSessionSummaries, isArchivedStore, unreadableStoreReason, } from "./private-local-session-discovery.js";
|
|
20
22
|
import { createPrivateLocalSessionRequestId, openPrivateLocalRuntimeHost, resolvePrivateLocalCreateStore, } from "./private-local-runtime-host.js";
|
|
21
23
|
import { parsePrivateLocalExecPath } from "./private-local-exec-path.js";
|
|
22
24
|
import { createLocalMeshLinks, localMeshLinkKey, } from "./private-local-mesh-links.js";
|
|
23
25
|
import { agentMailAddressSchema, machineAgentRequestSchema, machineDirectorySchema, } from "@fungi.computer/seam/machine-link-protocol";
|
|
24
|
-
import { commandSettlementSchema } from "@fungi.computer/shiitake/fleet";
|
|
26
|
+
import { commandSettlementSchema, settlementReadableBy, } from "@fungi.computer/shiitake/fleet";
|
|
25
27
|
import { BirdDogMailSchema } from "@fungi.computer/bird-dog/effect";
|
|
26
28
|
export class LocalSessionPathConflict extends Error {
|
|
27
29
|
name = "LocalSessionPathConflict";
|
|
@@ -72,7 +74,11 @@ readRecoveryManifest = (path) => readFile(path, "utf8"),
|
|
|
72
74
|
onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot(), meshLinksFactory = createLocalMeshLinks, onExecRuleMatch = () => undefined,
|
|
73
75
|
/** Supplied by the listener after startup; individual Agents never close it. */
|
|
74
76
|
serviceNative, onExecApprovalDecision = () => undefined) {
|
|
75
|
-
|
|
77
|
+
deferExtensionArchiveNotices(configRoot.home);
|
|
78
|
+
const extensions = suppliedExtensions ??
|
|
79
|
+
localExtensions({
|
|
80
|
+
home: configRoot.home,
|
|
81
|
+
});
|
|
76
82
|
const recoveryPath = recoveryDirectory === undefined
|
|
77
83
|
? undefined
|
|
78
84
|
: resolve(recoveryDirectory, "owners.json");
|
|
@@ -158,7 +164,7 @@ serviceNative, onExecApprovalDecision = () => undefined) {
|
|
|
158
164
|
return { status: "accepted", receipt: { sessionId } };
|
|
159
165
|
}
|
|
160
166
|
const facts = await admitted(owner, () => owner.readSettlement(operation.sessionId, operation.commandId));
|
|
161
|
-
if (facts !== undefined && facts
|
|
167
|
+
if (facts !== undefined && !settlementReadableBy(facts, actorAgentId))
|
|
162
168
|
return "refused";
|
|
163
169
|
return {
|
|
164
170
|
status: "accepted",
|
|
@@ -176,8 +182,9 @@ serviceNative, onExecApprovalDecision = () => undefined) {
|
|
|
176
182
|
envelope.key !== mail.key ||
|
|
177
183
|
mail.to.agentId !== address.agentId)
|
|
178
184
|
return "refused";
|
|
179
|
-
if (mail.from.
|
|
180
|
-
!(mail.
|
|
185
|
+
if (mail.from.kind === "system" &&
|
|
186
|
+
!(mail.reply?.outcome === "undeliverable" &&
|
|
187
|
+
mail.reply.reason === "recipient_offline"))
|
|
181
188
|
return "refused";
|
|
182
189
|
const sessionlessAcceptance = mail.to.sessionId === undefined;
|
|
183
190
|
// Only select the physical store here. Never bypass that runtime's
|
|
@@ -229,7 +236,9 @@ serviceNative, onExecApprovalDecision = () => undefined) {
|
|
|
229
236
|
for (const project of projects)
|
|
230
237
|
selections.set(project, await readProjectFungiMcpSelection(project));
|
|
231
238
|
const candidate = {
|
|
232
|
-
load: await localExtensions({
|
|
239
|
+
load: await localExtensions({
|
|
240
|
+
home: configRoot.home,
|
|
241
|
+
}).captureAgent(),
|
|
233
242
|
mcp: selections,
|
|
234
243
|
clientPath: path,
|
|
235
244
|
};
|
|
@@ -801,8 +810,9 @@ serviceNative, onExecApprovalDecision = () => undefined) {
|
|
|
801
810
|
const control = directory(invocationRoot);
|
|
802
811
|
return {
|
|
803
812
|
deliver: async (mail) => {
|
|
813
|
+
const senderAgentId = mail.from.kind === "agent" ? mail.from.agentId : undefined;
|
|
804
814
|
const remote = [...meshDirectories.values()].find(({ record }) => record.invocationRoot === invocationRoot &&
|
|
805
|
-
record.bindings.some((binding) => binding.agentId ===
|
|
815
|
+
record.bindings.some((binding) => binding.agentId === senderAgentId));
|
|
806
816
|
if (remote !== undefined) {
|
|
807
817
|
if (!remote.agents.some((agent) => agent.agentId === mail.to.agentId))
|
|
808
818
|
throw new Error("Mesh mail recipient is outside this Team");
|
|
@@ -893,6 +903,62 @@ serviceNative, onExecApprovalDecision = () => undefined) {
|
|
|
893
903
|
throw new AgentArchiveRefused("archived", `Agent ${agentId} is archived; unarchive it to work with it`);
|
|
894
904
|
throw new Error("Agent is outside this Local project");
|
|
895
905
|
}
|
|
906
|
+
/**
|
|
907
|
+
* Run one sender-authenticated command on a child's question owner. Which
|
|
908
|
+
* Agent the item waits on is decided by the child's own store; this door
|
|
909
|
+
* only states who is calling.
|
|
910
|
+
*/
|
|
911
|
+
async function callQuestionAsSender(target, senderAgentId, senderSessionId, command, commandInput, output, signal) {
|
|
912
|
+
ensureRunning();
|
|
913
|
+
signal.throwIfAborted();
|
|
914
|
+
const databasePath = (await targets(signal, "all")).get(target.agentId);
|
|
915
|
+
if (databasePath === undefined)
|
|
916
|
+
return outsideOrArchived(target.agentId, signal);
|
|
917
|
+
const { owner, release } = await admittedOwner(target.agentId, signal);
|
|
918
|
+
try {
|
|
919
|
+
return await admitted(owner, async () => Schema.decodeUnknownSync(output)(await owner
|
|
920
|
+
.getSession(target.sessionId)
|
|
921
|
+
.extension("botanical-session-question")
|
|
922
|
+
.call(command, commandInput, {
|
|
923
|
+
agentId: senderAgentId,
|
|
924
|
+
sessionId: senderSessionId,
|
|
925
|
+
})));
|
|
926
|
+
}
|
|
927
|
+
finally {
|
|
928
|
+
release();
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
async function answerAsSender(input, senderAgentId, senderSessionId, signal) {
|
|
932
|
+
const decoded = await callQuestionAsSender(input, senderAgentId, senderSessionId, "answerAgent", {
|
|
933
|
+
askId: input.askId,
|
|
934
|
+
requestId: input.requestId,
|
|
935
|
+
answers: input.answers,
|
|
936
|
+
}, questionContract.commands.answerAgent.output, signal);
|
|
937
|
+
const { result } = decoded;
|
|
938
|
+
if (result.result === "accepted")
|
|
939
|
+
return result.commandId === undefined
|
|
940
|
+
? { status: "answered", via: result.via }
|
|
941
|
+
: {
|
|
942
|
+
status: "answered",
|
|
943
|
+
via: result.via,
|
|
944
|
+
commandId: result.commandId,
|
|
945
|
+
};
|
|
946
|
+
if (result.result === "closed")
|
|
947
|
+
return { status: "closed", reason: result.status };
|
|
948
|
+
if (result.result === "refused")
|
|
949
|
+
return { status: "refused", reason: result.reason };
|
|
950
|
+
return { status: "not_found" };
|
|
951
|
+
}
|
|
952
|
+
async function escalateAsSender(input, senderAgentId, senderSessionId, signal) {
|
|
953
|
+
const decoded = await callQuestionAsSender(input, senderAgentId, senderSessionId, "escalateAgent", { askId: input.itemId }, questionContract.commands.escalateAgent.output, signal);
|
|
954
|
+
if (decoded.result === "escalated")
|
|
955
|
+
return { status: "escalated", to: decoded.to };
|
|
956
|
+
if (decoded.result === "closed")
|
|
957
|
+
return { status: "closed", reason: decoded.status };
|
|
958
|
+
if (decoded.result === "refused")
|
|
959
|
+
return { status: "refused", reason: decoded.reason };
|
|
960
|
+
return { status: "not_found" };
|
|
961
|
+
}
|
|
896
962
|
/**
|
|
897
963
|
* The first command still queued or running on any of an Agent's Sessions,
|
|
898
964
|
* so an archive can name it rather than cancel it.
|
|
@@ -969,6 +1035,8 @@ serviceNative, onExecApprovalDecision = () => undefined) {
|
|
|
969
1035
|
}
|
|
970
1036
|
return {
|
|
971
1037
|
owner: admittedOwner,
|
|
1038
|
+
answer: answerAsSender,
|
|
1039
|
+
escalate: escalateAsSender,
|
|
972
1040
|
archive: (request, signal) => setArchived(request, true, signal),
|
|
973
1041
|
unarchive: (request, signal) => setArchived(request, false, signal),
|
|
974
1042
|
list: async (signal, options) => {
|
|
@@ -1001,9 +1069,10 @@ serviceNative, onExecApprovalDecision = () => undefined) {
|
|
|
1001
1069
|
return model === null ? agent : { ...agent, model };
|
|
1002
1070
|
});
|
|
1003
1071
|
},
|
|
1004
|
-
create: async ({ requestId, name, model, extensions: requested = [], createdBy }, signal) => {
|
|
1072
|
+
create: async ({ requestId, name, model, extensions: requested = [], createdBy, createdBySessionId, }, signal) => {
|
|
1005
1073
|
ensureRunning();
|
|
1006
1074
|
signal.throwIfAborted();
|
|
1075
|
+
z.string().min(1).max(256).parse(createdBySessionId);
|
|
1007
1076
|
// An Agent creating an Agent cannot give it more than it holds. Refuse
|
|
1008
1077
|
// before anything exists; never trim the request.
|
|
1009
1078
|
const creatorDatabase = (await targets(signal)).get(createdBy);
|
|
@@ -1021,7 +1090,8 @@ serviceNative, onExecApprovalDecision = () => undefined) {
|
|
|
1021
1090
|
throw error;
|
|
1022
1091
|
}
|
|
1023
1092
|
const key = `create:${JSON.stringify([invocationRoot, requestId])}`;
|
|
1024
|
-
const
|
|
1093
|
+
const creator = { agentId: createdBy, sessionId: createdBySessionId };
|
|
1094
|
+
const createStore = await resolvePrivateLocalCreateStore(invocationRoot, requestId, creator);
|
|
1025
1095
|
signal.throwIfAborted();
|
|
1026
1096
|
ensureRunning();
|
|
1027
1097
|
const databasePath = createStore.databasePath;
|
|
@@ -1045,6 +1115,7 @@ serviceNative, onExecApprovalDecision = () => undefined) {
|
|
|
1045
1115
|
createRequestId: requestId,
|
|
1046
1116
|
name,
|
|
1047
1117
|
createdBy,
|
|
1118
|
+
createdBySessionId,
|
|
1048
1119
|
mode: creatorProfile.mode,
|
|
1049
1120
|
folder: creatorProfile.folder,
|
|
1050
1121
|
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
|
|
@@ -454,7 +466,13 @@ export function createPrivateLocalServiceManager(options = {}) {
|
|
|
454
466
|
// stopped it decides when it returns.
|
|
455
467
|
const ready = await status();
|
|
456
468
|
if (ready === undefined)
|
|
457
|
-
return {
|
|
469
|
+
return {
|
|
470
|
+
kind: "unavailable",
|
|
471
|
+
// The discovery owner's process-lifetime lease distinguishes an
|
|
472
|
+
// overloaded listener from a stopped service without guessing
|
|
473
|
+
// from the failed HTTP probe or an error message.
|
|
474
|
+
alive: await serviceOwnerIsLive(selectedDirectory),
|
|
475
|
+
};
|
|
458
476
|
accept(ready);
|
|
459
477
|
const again = await postSelection("select", {
|
|
460
478
|
invocationRoot: input.invocationRoot,
|
|
@@ -17,30 +17,41 @@ 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;
|
|
23
29
|
readonly name = "LocalStoreSchemaError";
|
|
24
30
|
constructor(databasePath: string, options?: ErrorOptions);
|
|
25
31
|
}
|
|
32
|
+
export type PrivateLocalAgentProfile = ReturnType<typeof openAgentProfile>;
|
|
26
33
|
/**
|
|
27
34
|
* One Agent store's chosen display name, default model, whether it is
|
|
28
35
|
* archived and the Agent that created it (absent when a human made it), read
|
|
29
|
-
* without opening a host.
|
|
36
|
+
* without opening a host. Directory listings and the sub-agents footer read
|
|
37
|
+
* every store's profile on each call; an unchanged store file answers from
|
|
38
|
+
* memory instead of reopening SQLite (each Bun open/close also leaks native
|
|
39
|
+
* memory).
|
|
30
40
|
*/
|
|
31
|
-
export declare function readPrivateLocalAgentProfile(path: string):
|
|
41
|
+
export declare function readPrivateLocalAgentProfile(path: string): PrivateLocalAgentProfile;
|
|
42
|
+
declare function openAgentProfile(path: string): Readonly<{
|
|
32
43
|
name: string | undefined;
|
|
33
44
|
createdBy: string | null;
|
|
34
45
|
archived: boolean;
|
|
35
46
|
mode: "read-only" | "workspace" | "full-access";
|
|
36
|
-
writableFolders: string[];
|
|
47
|
+
writableFolders: readonly string[];
|
|
37
48
|
folder: string;
|
|
38
49
|
model: {
|
|
39
50
|
readonly providerId: string;
|
|
40
51
|
readonly modelId: string;
|
|
41
52
|
readonly reasoning?: "high" | "low" | "max" | "medium" | "minimal" | "xhigh" | undefined;
|
|
42
53
|
} | null;
|
|
43
|
-
}
|
|
54
|
+
}>;
|
|
44
55
|
export declare function readPrivateLocalSessionSummaries(path: string): Promise<readonly LocalSummary[]>;
|
|
45
56
|
/**
|
|
46
57
|
* Discover existing folder sessions without opening or reconciling a host.
|
|
@@ -65,3 +76,4 @@ export declare function readPrivateLocalSessionDiscovery(invocationRoot: string,
|
|
|
65
76
|
}>;
|
|
66
77
|
/** Project the service-owned discovery into choices that resume through it. */
|
|
67
78
|
export declare function projectPrivateLocalSessionDiscovery(discovered: Awaited<ReturnType<typeof readPrivateLocalSessionDiscovery>>, connect: PrivateLocalServiceManager["connect"]): PrivateLocalSessionDiscovery;
|
|
79
|
+
export {};
|