@fungi.computer/truffle 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +294 -2
- package/computerd-loopback.cjs +16 -0
- package/dist/auth-cli.d.ts +36 -0
- package/dist/auth-cli.js +351 -0
- package/dist/fungi-account-command.d.ts +18 -0
- package/dist/fungi-account-command.js +92 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +17 -0
- package/dist/internal/private-workerd-sandbox-worker.mjs +27488 -0
- package/dist/internal/sandbox-metafile.json +6726 -0
- package/dist/linear-auth.d.ts +4 -0
- package/dist/linear-auth.js +72 -0
- package/dist/local-extensions.d.ts +97 -0
- package/dist/local-extensions.js +484 -0
- package/dist/local-login.d.ts +55 -0
- package/dist/local-login.js +167 -0
- package/dist/local-models.d.ts +14 -0
- package/dist/local-models.js +77 -0
- package/dist/local-skill-source.d.ts +6 -0
- package/dist/local-skill-source.js +357 -0
- package/dist/log-event.d.ts +2 -0
- package/dist/log-event.js +9 -0
- package/dist/private-bun-shiitake-host.d.ts +11 -0
- package/dist/private-bun-shiitake-host.js +66 -0
- package/dist/private-file-credential-store.d.ts +33 -0
- package/dist/private-file-credential-store.js +283 -0
- package/dist/private-files-patch.d.ts +3 -0
- package/dist/private-files-patch.js +86 -0
- package/dist/private-fungi-account.d.ts +235 -0
- package/dist/private-fungi-account.js +1149 -0
- package/dist/private-fungi-auth-error.d.ts +5 -0
- package/dist/private-fungi-auth-error.js +7 -0
- package/dist/private-fungi-mcp-connection.d.ts +22 -0
- package/dist/private-fungi-mcp-connection.js +143 -0
- package/dist/private-fungi-mcp-endpoint.d.ts +2 -0
- package/dist/private-fungi-mcp-endpoint.js +15 -0
- package/dist/private-fungi-mcp-selection.d.ts +14 -0
- package/dist/private-fungi-mcp-selection.js +36 -0
- package/dist/private-fungi-model-api.d.ts +6 -0
- package/dist/private-fungi-model-api.js +125 -0
- package/dist/private-fungi-model-catalog.d.ts +6 -0
- package/dist/private-fungi-model-catalog.js +78 -0
- package/dist/private-fungi-model-provider.d.ts +6 -0
- package/dist/private-fungi-model-provider.js +46 -0
- package/dist/private-fungi-pending-logouts.d.ts +34 -0
- package/dist/private-fungi-pending-logouts.js +136 -0
- package/dist/private-invocation-computer.d.ts +37 -0
- package/dist/private-invocation-computer.js +459 -0
- package/dist/private-invocation-files.d.ts +15 -0
- package/dist/private-invocation-files.js +449 -0
- package/dist/private-local-acp-stdio.d.ts +17 -0
- package/dist/private-local-acp-stdio.js +142 -0
- package/dist/private-local-auth-rpc.d.ts +69 -0
- package/dist/private-local-auth-rpc.js +273 -0
- package/dist/private-local-computer-attach.d.ts +49 -0
- package/dist/private-local-computer-attach.js +216 -0
- package/dist/private-local-computer-backend.d.ts +282 -0
- package/dist/private-local-computer-backend.js +402 -0
- package/dist/private-local-computer-processes.d.ts +199 -0
- package/dist/private-local-computer-processes.js +589 -0
- package/dist/private-local-default-model.d.ts +11 -0
- package/dist/private-local-default-model.js +45 -0
- package/dist/private-local-delegation.d.ts +29 -0
- package/dist/private-local-delegation.js +78 -0
- package/dist/private-local-effective-model.d.ts +19 -0
- package/dist/private-local-effective-model.js +14 -0
- package/dist/private-local-exec-launch.d.ts +41 -0
- package/dist/private-local-exec-launch.js +51 -0
- package/dist/private-local-exec-path.d.ts +6 -0
- package/dist/private-local-exec-path.js +40 -0
- package/dist/private-local-git-exclude.d.ts +2 -0
- package/dist/private-local-git-exclude.js +33 -0
- package/dist/private-local-http-dispatch.d.ts +5 -0
- package/dist/private-local-http-dispatch.js +14 -0
- package/dist/private-local-mail.d.ts +11 -0
- package/dist/private-local-mail.js +69 -0
- package/dist/private-local-model-settings.d.ts +5 -0
- package/dist/private-local-model-settings.js +31 -0
- package/dist/private-local-runtime-host.d.ts +141 -0
- package/dist/private-local-runtime-host.js +988 -0
- package/dist/private-local-service-contract.d.ts +90 -0
- package/dist/private-local-service-contract.js +1 -0
- package/dist/private-local-service-discovery.d.ts +101 -0
- package/dist/private-local-service-discovery.js +221 -0
- package/dist/private-local-service-ids.d.ts +3 -0
- package/dist/private-local-service-ids.js +8 -0
- package/dist/private-local-service-listener.d.ts +15 -0
- package/dist/private-local-service-listener.js +211 -0
- package/dist/private-local-service-owner.d.ts +1567 -0
- package/dist/private-local-service-owner.js +371 -0
- package/dist/private-local-service-process.d.ts +1 -0
- package/dist/private-local-service-process.js +6 -0
- package/dist/private-local-service.d.ts +17 -0
- package/dist/private-local-service.js +330 -0
- package/dist/private-local-session-discovery.d.ts +19 -0
- package/dist/private-local-session-discovery.js +157 -0
- package/dist/private-local-shiitake-host.d.ts +44 -0
- package/dist/private-local-shiitake-host.js +143 -0
- package/dist/private-local-subagents.d.ts +27 -0
- package/dist/private-local-subagents.js +77 -0
- package/dist/private-local-worker-server.d.ts +15 -0
- package/dist/private-local-worker-server.js +136 -0
- package/dist/private-sqlite-owner.d.ts +7 -0
- package/dist/private-sqlite-owner.js +103 -0
- package/dist/private-team-computers.d.ts +38 -0
- package/dist/private-team-computers.js +101 -0
- package/dist/private-workerd-sandbox.d.ts +3 -0
- package/dist/private-workerd-sandbox.js +347 -0
- package/dist/team-apps.d.ts +24 -0
- package/dist/team-apps.js +123 -0
- package/dist/tracker-git-credential.d.ts +15 -0
- package/dist/tracker-git-credential.js +87 -0
- package/native/bin/build.json +4037 -0
- package/native/bin/fungi-apply-patch +0 -0
- package/package.json +148 -3
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { PrivateFungiMcpSelection } from "./private-fungi-mcp-selection.js";
|
|
2
|
+
import type { AuthInteraction } from "@earendil-works/pi-ai";
|
|
3
|
+
import type { PrivateLocalAuthRequest } from "./private-local-auth-rpc.js";
|
|
4
|
+
import type { LocalAuthResult } from "./auth-cli.js";
|
|
5
|
+
import type { SessionSnapshot, ShiitakeAgent, ShiitakeSession } from "@fungi.computer/shiitake";
|
|
6
|
+
import type { PrivateLocalModelChoice } from "./local-models.js";
|
|
7
|
+
import type { PrivateLocalEffectiveModel } from "./private-local-effective-model.js";
|
|
8
|
+
/** Service identity is checked by handshake, never inferred from a discovery PID. */
|
|
9
|
+
export type PrivateLocalServiceStatus = Readonly<{
|
|
10
|
+
serviceId: string;
|
|
11
|
+
baseUrl: string;
|
|
12
|
+
/** Captured from the packed CLI at service launch. */
|
|
13
|
+
buildId?: string | undefined;
|
|
14
|
+
}>;
|
|
15
|
+
export type PrivateLocalServiceStopResult = Readonly<{
|
|
16
|
+
kind: "stopped";
|
|
17
|
+
}> | Readonly<{
|
|
18
|
+
kind: "not-running";
|
|
19
|
+
recordCleaned: boolean;
|
|
20
|
+
}> | Readonly<{
|
|
21
|
+
kind: "replaced";
|
|
22
|
+
}>;
|
|
23
|
+
/** Selection addresses an existing Agent store; Session identity is not a new store. */
|
|
24
|
+
export type PrivateLocalServiceSelection = Readonly<{
|
|
25
|
+
invocationRoot: string;
|
|
26
|
+
clientPath?: string | undefined;
|
|
27
|
+
databasePath?: string | undefined;
|
|
28
|
+
sessionId?: string | undefined;
|
|
29
|
+
fungiMcp?: PrivateFungiMcpSelection | undefined;
|
|
30
|
+
}>;
|
|
31
|
+
export type PrivateLocalSessionCreation = Readonly<{
|
|
32
|
+
invocationRoot: string;
|
|
33
|
+
clientPath?: string | undefined;
|
|
34
|
+
databasePath: string;
|
|
35
|
+
requestId: string;
|
|
36
|
+
fungiMcp?: PrivateFungiMcpSelection | undefined;
|
|
37
|
+
}>;
|
|
38
|
+
export type PrivateLocalServiceRequestOptions = Readonly<{
|
|
39
|
+
signal?: AbortSignal | undefined;
|
|
40
|
+
}>;
|
|
41
|
+
export type PrivateLocalSessionInspection = Readonly<{
|
|
42
|
+
session: ShiitakeSession;
|
|
43
|
+
snapshot: SessionSnapshot;
|
|
44
|
+
close: () => Promise<void>;
|
|
45
|
+
}>;
|
|
46
|
+
export type PrivateLocalServiceConnection = Readonly<{
|
|
47
|
+
service: PrivateLocalServiceStatus;
|
|
48
|
+
agentId: string;
|
|
49
|
+
databasePath: string;
|
|
50
|
+
sessionId: string;
|
|
51
|
+
client: ShiitakeAgent;
|
|
52
|
+
session: ShiitakeSession;
|
|
53
|
+
effectiveModel: (options?: PrivateLocalServiceRequestOptions) => Promise<PrivateLocalEffectiveModel>;
|
|
54
|
+
/** Releases this client's watches only. Work and service ownership remain alive. */
|
|
55
|
+
close: () => Promise<void>;
|
|
56
|
+
}>;
|
|
57
|
+
export type PrivateLocalSessionChoice = Readonly<{
|
|
58
|
+
databasePath: string;
|
|
59
|
+
sessionId: string;
|
|
60
|
+
title: string;
|
|
61
|
+
createdAt: number;
|
|
62
|
+
updatedAt: number;
|
|
63
|
+
/** Reopen this saved store through the persistent service connection. */
|
|
64
|
+
resume: (options?: PrivateLocalServiceRequestOptions & {
|
|
65
|
+
fungiMcp?: PrivateFungiMcpSelection | undefined;
|
|
66
|
+
}) => Promise<PrivateLocalServiceConnection>;
|
|
67
|
+
}>;
|
|
68
|
+
export type PrivateLocalSessionProblem = Readonly<{
|
|
69
|
+
kind: "corrupt" | "in_use" | "unreadable";
|
|
70
|
+
}>;
|
|
71
|
+
export type PrivateLocalSessionDiscovery = Readonly<{
|
|
72
|
+
agentCount: number;
|
|
73
|
+
sessions: readonly PrivateLocalSessionChoice[];
|
|
74
|
+
problems: readonly PrivateLocalSessionProblem[];
|
|
75
|
+
}>;
|
|
76
|
+
/** Local process management stays outside the shared Session HTTP contract. */
|
|
77
|
+
export type PrivateLocalServiceManager = Readonly<{
|
|
78
|
+
status: () => Promise<PrivateLocalServiceStatus | undefined>;
|
|
79
|
+
start: (options?: PrivateLocalServiceRequestOptions) => Promise<PrivateLocalServiceStatus>;
|
|
80
|
+
createSession: (creation: PrivateLocalSessionCreation, options?: PrivateLocalServiceRequestOptions) => Promise<PrivateLocalServiceConnection>;
|
|
81
|
+
connect: (selection: PrivateLocalServiceSelection, options?: PrivateLocalServiceRequestOptions) => Promise<PrivateLocalServiceConnection>;
|
|
82
|
+
discover: (invocationRoot: string, options?: PrivateLocalServiceRequestOptions) => Promise<PrivateLocalSessionDiscovery>;
|
|
83
|
+
inspect: (selection: Omit<PrivateLocalServiceSelection, "clientPath">, options?: PrivateLocalServiceRequestOptions & Readonly<{
|
|
84
|
+
limit?: number | undefined;
|
|
85
|
+
}>) => Promise<PrivateLocalSessionInspection>;
|
|
86
|
+
auth: (request: PrivateLocalAuthRequest, interaction: AuthInteraction) => Promise<LocalAuthResult>;
|
|
87
|
+
models: (options?: PrivateLocalServiceRequestOptions) => Promise<readonly PrivateLocalModelChoice[]>;
|
|
88
|
+
/** An expected identity avoids stopping a replacement started by another client. */
|
|
89
|
+
stop: (expectedServiceId?: string) => Promise<PrivateLocalServiceStopResult>;
|
|
90
|
+
}>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
declare const serviceRecordSchema: z.ZodObject<{
|
|
3
|
+
serviceId: z.ZodString & z.ZodType<import("@fungi.computer/cairn").CairnId<"local-service", "service", 1>, string, z.core.$ZodTypeInternals<import("@fungi.computer/cairn").CairnId<"local-service", "service", 1>, string>>;
|
|
4
|
+
baseUrl: z.ZodURL;
|
|
5
|
+
}, z.core.$strict>;
|
|
6
|
+
declare const serviceStatusSchema: z.ZodObject<{
|
|
7
|
+
serviceId: z.ZodString & z.ZodType<import("@fungi.computer/cairn").CairnId<"local-service", "service", 1>, string, z.core.$ZodTypeInternals<import("@fungi.computer/cairn").CairnId<"local-service", "service", 1>, string>>;
|
|
8
|
+
baseUrl: z.ZodURL;
|
|
9
|
+
buildId: z.ZodOptional<z.ZodString>;
|
|
10
|
+
}, z.core.$strict>;
|
|
11
|
+
export type ServiceRecord = z.infer<typeof serviceRecordSchema>;
|
|
12
|
+
export declare const selectionSchema: z.ZodObject<{
|
|
13
|
+
invocationRoot: z.ZodString;
|
|
14
|
+
clientPath: z.ZodString;
|
|
15
|
+
databasePath: z.ZodOptional<z.ZodString>;
|
|
16
|
+
sessionId: z.ZodOptional<z.ZodString>;
|
|
17
|
+
fungiMcp: z.ZodOptional<z.ZodObject<{
|
|
18
|
+
endpoint: z.ZodPipe<z.ZodURL, z.ZodString>;
|
|
19
|
+
}, z.core.$strict>>;
|
|
20
|
+
}, z.core.$strict>;
|
|
21
|
+
export declare const sessionCreationSchema: z.ZodObject<{
|
|
22
|
+
invocationRoot: z.ZodString;
|
|
23
|
+
clientPath: z.ZodString;
|
|
24
|
+
databasePath: z.ZodString;
|
|
25
|
+
requestId: z.ZodString;
|
|
26
|
+
fungiMcp: z.ZodOptional<z.ZodObject<{
|
|
27
|
+
endpoint: z.ZodPipe<z.ZodURL, z.ZodString>;
|
|
28
|
+
}, z.core.$strict>>;
|
|
29
|
+
}, z.core.$strict>;
|
|
30
|
+
export declare const sessionInspectionSchema: z.ZodObject<{
|
|
31
|
+
fungiMcp: z.ZodOptional<z.ZodObject<{
|
|
32
|
+
endpoint: z.ZodPipe<z.ZodURL, z.ZodString>;
|
|
33
|
+
}, z.core.$strict>>;
|
|
34
|
+
invocationRoot: z.ZodString;
|
|
35
|
+
databasePath: z.ZodOptional<z.ZodString>;
|
|
36
|
+
sessionId: z.ZodString;
|
|
37
|
+
limit: z.ZodNumber;
|
|
38
|
+
}, z.core.$strict>;
|
|
39
|
+
export declare const selectedSchema: z.ZodObject<{
|
|
40
|
+
agentId: z.ZodString;
|
|
41
|
+
databasePath: z.ZodString;
|
|
42
|
+
sessionId: z.ZodString;
|
|
43
|
+
routePath: z.ZodString;
|
|
44
|
+
}, z.core.$strict>;
|
|
45
|
+
export declare const sessionDiscoverySchema: z.ZodObject<{
|
|
46
|
+
root: z.ZodString;
|
|
47
|
+
agentCount: z.ZodNumber;
|
|
48
|
+
sessions: z.ZodArray<z.ZodObject<{
|
|
49
|
+
databasePath: z.ZodString;
|
|
50
|
+
sessionId: z.ZodString;
|
|
51
|
+
title: z.ZodString;
|
|
52
|
+
createdAt: z.ZodNumber;
|
|
53
|
+
updatedAt: z.ZodNumber;
|
|
54
|
+
}, z.core.$strict>>;
|
|
55
|
+
problems: z.ZodArray<z.ZodObject<{
|
|
56
|
+
kind: z.ZodEnum<{
|
|
57
|
+
corrupt: "corrupt";
|
|
58
|
+
in_use: "in_use";
|
|
59
|
+
unreadable: "unreadable";
|
|
60
|
+
}>;
|
|
61
|
+
}, z.core.$strict>>;
|
|
62
|
+
}, z.core.$strict>;
|
|
63
|
+
export declare const modelChoicesSchema: z.ZodArray<z.ZodObject<{
|
|
64
|
+
providerId: z.ZodString;
|
|
65
|
+
providerName: z.ZodString;
|
|
66
|
+
id: z.ZodString;
|
|
67
|
+
name: z.ZodString;
|
|
68
|
+
reasoning: z.ZodArray<z.ZodEnum<{
|
|
69
|
+
minimal: "minimal";
|
|
70
|
+
low: "low";
|
|
71
|
+
medium: "medium";
|
|
72
|
+
high: "high";
|
|
73
|
+
xhigh: "xhigh";
|
|
74
|
+
max: "max";
|
|
75
|
+
}>>;
|
|
76
|
+
}, z.core.$strict>>;
|
|
77
|
+
export declare function serviceDirectory(directory?: string): Promise<string>;
|
|
78
|
+
export declare const recordPath: (directory: string) => string;
|
|
79
|
+
export declare function readServiceRecord(directory: string): Promise<ServiceRecord | undefined>;
|
|
80
|
+
/** An older listener must not erase its successor's discovery record. */
|
|
81
|
+
export declare function removeOwnedServiceRecord(directory: string, serviceId: string): Promise<void>;
|
|
82
|
+
/** Legacy owners have no SQLite lease, so their file lock must not be stolen. */
|
|
83
|
+
export declare function assertNoUntrackedServiceOwner(directory: string): Promise<void>;
|
|
84
|
+
/** Startup and shutdown serialize with the native listener and lifetime owner lock. */
|
|
85
|
+
export declare function acquireServiceLock(directory: string, name: "startup" | "owner"): Promise<() => Promise<void>>;
|
|
86
|
+
/**
|
|
87
|
+
* Whether a service process holds this directory's lifetime lease. A held
|
|
88
|
+
* lease is a live owner even when it is too busy to answer its handshake.
|
|
89
|
+
*/
|
|
90
|
+
export declare function serviceOwnerIsLive(directory: string): boolean;
|
|
91
|
+
export type ServiceProbe = Readonly<{
|
|
92
|
+
kind: "running";
|
|
93
|
+
service: z.infer<typeof serviceStatusSchema>;
|
|
94
|
+
}> | Readonly<{
|
|
95
|
+
kind: "not-running";
|
|
96
|
+
}> | Readonly<{
|
|
97
|
+
kind: "unavailable";
|
|
98
|
+
}>;
|
|
99
|
+
export declare function readLiveServiceStatus(record: ServiceRecord, timeoutMs?: number): Promise<ServiceProbe>;
|
|
100
|
+
export declare function handshake(record: ServiceRecord): Promise<boolean>;
|
|
101
|
+
export {};
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { fungiMcpSelectionSchema } from "./private-fungi-mcp-selection.js";
|
|
2
|
+
import { privateLocalExecPathSchema } from "./private-local-exec-path.js";
|
|
3
|
+
import { chmod, mkdir, readFile, realpath, rm, rmdir, stat, writeFile, } from "node:fs/promises";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import lockfile from "proper-lockfile";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { localServiceIds } from "./private-local-service-ids.js";
|
|
9
|
+
import { acquireNodeStoreLease } from "@fungi.computer/shiitake/node";
|
|
10
|
+
import { privateSqliteFailureCode } from "./private-sqlite-owner.js";
|
|
11
|
+
const serviceRecordSchema = z.strictObject({
|
|
12
|
+
serviceId: z.string().refine((value) => localServiceIds.service.is(value)),
|
|
13
|
+
baseUrl: z.url().refine((value) => {
|
|
14
|
+
const url = new URL(value);
|
|
15
|
+
return (url.protocol === "http:" &&
|
|
16
|
+
url.hostname === "127.0.0.1" &&
|
|
17
|
+
url.port !== "" &&
|
|
18
|
+
url.username === "" &&
|
|
19
|
+
url.password === "" &&
|
|
20
|
+
url.pathname === "/" &&
|
|
21
|
+
url.search === "" &&
|
|
22
|
+
url.hash === "");
|
|
23
|
+
}),
|
|
24
|
+
});
|
|
25
|
+
const serviceStatusSchema = serviceRecordSchema.safeExtend({
|
|
26
|
+
buildId: z
|
|
27
|
+
.string()
|
|
28
|
+
.regex(/^[a-f0-9]{64}$/u)
|
|
29
|
+
.optional(),
|
|
30
|
+
});
|
|
31
|
+
export const selectionSchema = z.strictObject({
|
|
32
|
+
invocationRoot: z.string().min(1),
|
|
33
|
+
clientPath: privateLocalExecPathSchema,
|
|
34
|
+
databasePath: z.string().min(1).optional(),
|
|
35
|
+
sessionId: z.string().min(1).optional(),
|
|
36
|
+
fungiMcp: fungiMcpSelectionSchema.optional(),
|
|
37
|
+
});
|
|
38
|
+
export const sessionCreationSchema = z.strictObject({
|
|
39
|
+
invocationRoot: z.string().min(1),
|
|
40
|
+
clientPath: privateLocalExecPathSchema,
|
|
41
|
+
databasePath: z.string().min(1),
|
|
42
|
+
requestId: z.string().min(1),
|
|
43
|
+
fungiMcp: fungiMcpSelectionSchema.optional(),
|
|
44
|
+
});
|
|
45
|
+
export const sessionInspectionSchema = selectionSchema
|
|
46
|
+
.omit({ clientPath: true })
|
|
47
|
+
.extend({
|
|
48
|
+
sessionId: z.string().min(1),
|
|
49
|
+
limit: z.number().int().min(1).max(200),
|
|
50
|
+
});
|
|
51
|
+
export const selectedSchema = z.strictObject({
|
|
52
|
+
agentId: z.string().min(1),
|
|
53
|
+
databasePath: z.string().min(1),
|
|
54
|
+
sessionId: z.string().min(1),
|
|
55
|
+
routePath: z.string().regex(/^\/agents\/[0-9]+$/),
|
|
56
|
+
});
|
|
57
|
+
export const sessionDiscoverySchema = z.strictObject({
|
|
58
|
+
root: z.string().min(1),
|
|
59
|
+
agentCount: z.number().int().nonnegative(),
|
|
60
|
+
sessions: z.array(z.strictObject({
|
|
61
|
+
databasePath: z.string().min(1),
|
|
62
|
+
sessionId: z.string().min(1),
|
|
63
|
+
title: z.string(),
|
|
64
|
+
createdAt: z.number(),
|
|
65
|
+
updatedAt: z.number(),
|
|
66
|
+
})),
|
|
67
|
+
problems: z.array(z.strictObject({ kind: z.enum(["corrupt", "in_use", "unreadable"]) })),
|
|
68
|
+
});
|
|
69
|
+
const reasoningLevels = [
|
|
70
|
+
"minimal",
|
|
71
|
+
"low",
|
|
72
|
+
"medium",
|
|
73
|
+
"high",
|
|
74
|
+
"xhigh",
|
|
75
|
+
"max",
|
|
76
|
+
];
|
|
77
|
+
export const modelChoicesSchema = z.array(z.strictObject({
|
|
78
|
+
providerId: z.string().min(1),
|
|
79
|
+
providerName: z.string().min(1),
|
|
80
|
+
id: z.string().min(1),
|
|
81
|
+
name: z.string().min(1),
|
|
82
|
+
reasoning: z.array(z.enum(reasoningLevels)),
|
|
83
|
+
}));
|
|
84
|
+
export async function serviceDirectory(directory) {
|
|
85
|
+
const path = resolve(directory ?? join(homedir(), ".fungi", "service"));
|
|
86
|
+
await mkdir(path, { recursive: true, mode: 0o700 });
|
|
87
|
+
await chmod(path, 0o700);
|
|
88
|
+
return realpath(path);
|
|
89
|
+
}
|
|
90
|
+
export const recordPath = (directory) => join(directory, "service.json");
|
|
91
|
+
export async function readServiceRecord(directory) {
|
|
92
|
+
try {
|
|
93
|
+
return serviceRecordSchema.parse(JSON.parse(await readFile(recordPath(directory), "utf8")));
|
|
94
|
+
}
|
|
95
|
+
catch (cause) {
|
|
96
|
+
if (cause instanceof Error && "code" in cause && cause.code === "ENOENT")
|
|
97
|
+
return undefined;
|
|
98
|
+
throw cause;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** An older listener must not erase its successor's discovery record. */
|
|
102
|
+
export async function removeOwnedServiceRecord(directory, serviceId) {
|
|
103
|
+
if ((await readServiceRecord(directory))?.serviceId === serviceId)
|
|
104
|
+
await rm(recordPath(directory), { force: true });
|
|
105
|
+
}
|
|
106
|
+
const ownerLeaseMarker = (directory) => join(directory, "owner.lock", "sqlite-owner");
|
|
107
|
+
async function pathExists(path) {
|
|
108
|
+
try {
|
|
109
|
+
await stat(path);
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
catch (cause) {
|
|
113
|
+
if (cause instanceof Error && "code" in cause && cause.code === "ENOENT")
|
|
114
|
+
return false;
|
|
115
|
+
throw cause;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** Legacy owners have no SQLite lease, so their file lock must not be stolen. */
|
|
119
|
+
export async function assertNoUntrackedServiceOwner(directory) {
|
|
120
|
+
if ((await pathExists(join(directory, "owner.lock"))) &&
|
|
121
|
+
!(await pathExists(ownerLeaseMarker(directory))))
|
|
122
|
+
throw new Error("A previous Fungi service may still own this directory. Stop that service before starting Fungi again; its owner lock was not released.");
|
|
123
|
+
}
|
|
124
|
+
/** Startup and shutdown serialize with the native listener and lifetime owner lock. */
|
|
125
|
+
export async function acquireServiceLock(directory, name) {
|
|
126
|
+
if (name === "startup")
|
|
127
|
+
return lockfile.lock(join(directory, name), {
|
|
128
|
+
realpath: false,
|
|
129
|
+
lockfilePath: join(directory, `${name}.lock`),
|
|
130
|
+
retries: { retries: 100, minTimeout: 100, maxTimeout: 100 },
|
|
131
|
+
onCompromised(error) {
|
|
132
|
+
throw error;
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
// The SQLite lease is the lifetime guard. proper-lockfile cannot heartbeat
|
|
136
|
+
// a directory containing the marker: writing the marker changes its mtime
|
|
137
|
+
// and makes its own heartbeat declare the live lock compromised.
|
|
138
|
+
const lease = acquireNodeStoreLease(join(directory, "owner.sqlite"));
|
|
139
|
+
const lockPath = join(directory, "owner.lock");
|
|
140
|
+
let created = false;
|
|
141
|
+
try {
|
|
142
|
+
await assertNoUntrackedServiceOwner(directory);
|
|
143
|
+
// A dead updated owner can leave a marked directory behind. The SQLite
|
|
144
|
+
// lease is already free, so removing it does not trust a heartbeat.
|
|
145
|
+
if (await pathExists(ownerLeaseMarker(directory)))
|
|
146
|
+
await rm(lockPath, { recursive: true });
|
|
147
|
+
await mkdir(lockPath);
|
|
148
|
+
created = true;
|
|
149
|
+
await writeFile(ownerLeaseMarker(directory), "", {
|
|
150
|
+
flag: "wx",
|
|
151
|
+
mode: 0o600,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
catch (cause) {
|
|
155
|
+
try {
|
|
156
|
+
if (created)
|
|
157
|
+
await rm(lockPath, { recursive: true });
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
lease.release();
|
|
161
|
+
}
|
|
162
|
+
throw cause;
|
|
163
|
+
}
|
|
164
|
+
return async () => {
|
|
165
|
+
try {
|
|
166
|
+
await rm(ownerLeaseMarker(directory), { force: true });
|
|
167
|
+
await rmdir(lockPath);
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
lease.release();
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Whether a service process holds this directory's lifetime lease. A held
|
|
176
|
+
* lease is a live owner even when it is too busy to answer its handshake.
|
|
177
|
+
*/
|
|
178
|
+
export function serviceOwnerIsLive(directory) {
|
|
179
|
+
let lease;
|
|
180
|
+
try {
|
|
181
|
+
lease = acquireNodeStoreLease(join(directory, "owner.sqlite"));
|
|
182
|
+
}
|
|
183
|
+
catch (cause) {
|
|
184
|
+
if (privateSqliteFailureCode(cause) === "busy")
|
|
185
|
+
return true;
|
|
186
|
+
throw cause;
|
|
187
|
+
}
|
|
188
|
+
lease.release();
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
const transportFailureSchema = z.looseObject({
|
|
192
|
+
code: z.string().optional(),
|
|
193
|
+
cause: z.looseObject({ code: z.string().optional() }).optional(),
|
|
194
|
+
});
|
|
195
|
+
export async function readLiveServiceStatus(record, timeoutMs = 2000) {
|
|
196
|
+
try {
|
|
197
|
+
const response = await fetch(`${record.baseUrl}/service/${record.serviceId}/status`, {
|
|
198
|
+
redirect: "error",
|
|
199
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
200
|
+
});
|
|
201
|
+
if (!response.ok)
|
|
202
|
+
return { kind: "unavailable" };
|
|
203
|
+
const actual = serviceStatusSchema.parse(await response.json());
|
|
204
|
+
return actual.serviceId === record.serviceId &&
|
|
205
|
+
actual.baseUrl === record.baseUrl
|
|
206
|
+
? { kind: "running", service: actual }
|
|
207
|
+
: { kind: "unavailable" };
|
|
208
|
+
}
|
|
209
|
+
catch (cause) {
|
|
210
|
+
const failure = transportFailureSchema.safeParse(cause);
|
|
211
|
+
if (failure.success) {
|
|
212
|
+
const codes = [failure.data.code, failure.data.cause?.code];
|
|
213
|
+
if (codes.includes("ECONNREFUSED") || codes.includes("ConnectionRefused"))
|
|
214
|
+
return { kind: "not-running" };
|
|
215
|
+
}
|
|
216
|
+
return { kind: "unavailable" };
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
export async function handshake(record) {
|
|
220
|
+
return (await readLiveServiceStatus(record)).kind === "running";
|
|
221
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { runLocalAuthCli } from "./auth-cli.js";
|
|
2
|
+
import { listPrivateModels } from "./local-models.js";
|
|
3
|
+
import { type OpenLocalServiceRuntime } from "./private-local-service-owner.js";
|
|
4
|
+
export declare function createPrivateLocalServiceShutdown(input: {
|
|
5
|
+
closeOwners: () => Promise<void>;
|
|
6
|
+
closeServer: () => Promise<void>;
|
|
7
|
+
removeRecord: () => Promise<void>;
|
|
8
|
+
releaseOwner: () => Promise<void>;
|
|
9
|
+
}): {
|
|
10
|
+
drain: () => Promise<void>;
|
|
11
|
+
finish: () => Promise<void>;
|
|
12
|
+
isDraining: () => boolean;
|
|
13
|
+
};
|
|
14
|
+
/** Native Bun listener retained from TUI; service owns both listener and stores. */
|
|
15
|
+
export declare function runPrivateLocalService(directory: string, openRuntime?: OpenLocalServiceRuntime, listModels?: typeof listPrivateModels, runAuth?: typeof runLocalAuthCli): Promise<void>;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { FungiMcpSelectionConflict } from "./private-fungi-mcp-selection.js";
|
|
2
|
+
import { PrivateLocalAuthTarget } from "./private-local-auth-rpc.js";
|
|
3
|
+
import { runLocalAuthCli } from "./auth-cli.js";
|
|
4
|
+
import { Hono } from "hono";
|
|
5
|
+
import { localServiceIds } from "./private-local-service-ids.js";
|
|
6
|
+
import writeFileAtomic from "write-file-atomic";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { listPrivateModels } from "./local-models.js";
|
|
9
|
+
import { privateSqliteFailureCode } from "./private-sqlite-owner.js";
|
|
10
|
+
import { openPrivateLocalBunServer } from "./private-bun-shiitake-host.js";
|
|
11
|
+
import { readPrivateLocalSessionDiscovery } from "./private-local-session-discovery.js";
|
|
12
|
+
import { createLocalServiceOwners, LocalSessionPathConflict, } from "./private-local-service-owner.js";
|
|
13
|
+
import { acquireServiceLock, recordPath, removeOwnedServiceRecord, selectionSchema, sessionCreationSchema, sessionInspectionSchema, } from "./private-local-service-discovery.js";
|
|
14
|
+
export function createPrivateLocalServiceShutdown(input) {
|
|
15
|
+
let draining;
|
|
16
|
+
let finishing;
|
|
17
|
+
const drain = () => {
|
|
18
|
+
draining ??= input.closeOwners();
|
|
19
|
+
return draining;
|
|
20
|
+
};
|
|
21
|
+
const finish = () => {
|
|
22
|
+
finishing ??= (async () => {
|
|
23
|
+
// Failed owner drain leaves the live listener, discovery, and owner lock
|
|
24
|
+
// intact. A caller must never observe this service as stopped.
|
|
25
|
+
await drain();
|
|
26
|
+
await input.closeServer();
|
|
27
|
+
try {
|
|
28
|
+
await input.removeRecord();
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
await input.releaseOwner();
|
|
32
|
+
}
|
|
33
|
+
})();
|
|
34
|
+
return finishing;
|
|
35
|
+
};
|
|
36
|
+
return { drain, finish, isDraining: () => draining !== undefined };
|
|
37
|
+
}
|
|
38
|
+
/** Native Bun listener retained from TUI; service owns both listener and stores. */
|
|
39
|
+
export async function runPrivateLocalService(directory, openRuntime, listModels = listPrivateModels, runAuth = runLocalAuthCli) {
|
|
40
|
+
const release = await acquireServiceLock(directory, "owner");
|
|
41
|
+
const owners = createLocalServiceOwners(openRuntime, directory);
|
|
42
|
+
const serviceId = localServiceIds.service.generate();
|
|
43
|
+
const buildId = z
|
|
44
|
+
.string()
|
|
45
|
+
.regex(/^[a-f0-9]{64}$/u)
|
|
46
|
+
.optional()
|
|
47
|
+
.parse(process.env.FUNGI_BUNDLED_BUILD_ID);
|
|
48
|
+
const prefix = `/service/${serviceId}`;
|
|
49
|
+
const app = new Hono();
|
|
50
|
+
app.onError((error, context) => {
|
|
51
|
+
if (error instanceof z.ZodError)
|
|
52
|
+
return context.json({
|
|
53
|
+
code: "invalid_selection",
|
|
54
|
+
issues: error.issues.map((issue) => ({
|
|
55
|
+
field: issue.path.join("."),
|
|
56
|
+
reason: issue.message,
|
|
57
|
+
})),
|
|
58
|
+
}, 400);
|
|
59
|
+
if (error instanceof LocalSessionPathConflict)
|
|
60
|
+
return context.json({ code: "session_path_conflict", error: error.message }, 409);
|
|
61
|
+
if (error instanceof FungiMcpSelectionConflict)
|
|
62
|
+
return context.json({ error: error.message }, 409);
|
|
63
|
+
const sqliteFailure = privateSqliteFailureCode(error);
|
|
64
|
+
if (sqliteFailure === "busy" || sqliteFailure === "locked")
|
|
65
|
+
return context.json({ code: "store_in_use" }, 409);
|
|
66
|
+
console.error(error);
|
|
67
|
+
return context.text("Internal Server Error", 500);
|
|
68
|
+
});
|
|
69
|
+
const routes = new Map();
|
|
70
|
+
const stopped = Promise.withResolvers();
|
|
71
|
+
let server;
|
|
72
|
+
let recordWritten = false;
|
|
73
|
+
const mount = (selected, sessionId) => {
|
|
74
|
+
let mounted = routes.get(selected.databasePath);
|
|
75
|
+
if (!mounted) {
|
|
76
|
+
const path = `/agents/${String(routes.size)}`;
|
|
77
|
+
const routePrefix = prefix + path;
|
|
78
|
+
mounted = {
|
|
79
|
+
path,
|
|
80
|
+
fetch: (request) => {
|
|
81
|
+
const url = new URL(request.url);
|
|
82
|
+
url.pathname = url.pathname.slice(routePrefix.length) || "/";
|
|
83
|
+
return selected.routes.fetch(new Request(url.toString(), request));
|
|
84
|
+
},
|
|
85
|
+
effectiveModel: selected.effectiveModel,
|
|
86
|
+
};
|
|
87
|
+
routes.set(selected.databasePath, mounted);
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
agentId: selected.agentId,
|
|
91
|
+
databasePath: selected.databasePath,
|
|
92
|
+
sessionId,
|
|
93
|
+
routePath: mounted.path,
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
const shutdown = createPrivateLocalServiceShutdown({
|
|
97
|
+
closeOwners: owners.close,
|
|
98
|
+
closeServer: async () => {
|
|
99
|
+
await server?.close();
|
|
100
|
+
},
|
|
101
|
+
removeRecord: async () => {
|
|
102
|
+
if (recordWritten)
|
|
103
|
+
await removeOwnedServiceRecord(directory, serviceId);
|
|
104
|
+
},
|
|
105
|
+
releaseOwner: release,
|
|
106
|
+
});
|
|
107
|
+
const finish = () => {
|
|
108
|
+
const finishing = shutdown.finish();
|
|
109
|
+
void finishing.then(() => {
|
|
110
|
+
stopped.resolve(undefined);
|
|
111
|
+
}, (cause) => {
|
|
112
|
+
stopped.reject(cause);
|
|
113
|
+
});
|
|
114
|
+
return finishing;
|
|
115
|
+
};
|
|
116
|
+
const onSignal = () => {
|
|
117
|
+
void finish();
|
|
118
|
+
};
|
|
119
|
+
const isLocalRequest = (request) => !request.headers.has("origin") &&
|
|
120
|
+
new URL(request.url).hostname === "127.0.0.1";
|
|
121
|
+
try {
|
|
122
|
+
await owners.restore();
|
|
123
|
+
app.use("*", async (context, next) => {
|
|
124
|
+
const request = context.req.raw;
|
|
125
|
+
// Local client only: no browser-origin admission or DNS rebinding authority.
|
|
126
|
+
if (!isLocalRequest(request))
|
|
127
|
+
return context.body(null, 403);
|
|
128
|
+
const path = new URL(request.url).pathname;
|
|
129
|
+
if (shutdown.isDraining() &&
|
|
130
|
+
path !== `${prefix}/status` &&
|
|
131
|
+
path !== `${prefix}/stop`)
|
|
132
|
+
return context.body(null, 503);
|
|
133
|
+
await next();
|
|
134
|
+
return;
|
|
135
|
+
});
|
|
136
|
+
app.get(`${prefix}/status`, (context) => context.json({ serviceId, baseUrl: server?.baseUrl, buildId }));
|
|
137
|
+
app.get(`${prefix}/models`, async (context) => context.json(await listModels()));
|
|
138
|
+
app.post(`${prefix}/discover`, async (context) => {
|
|
139
|
+
const input = selectionSchema
|
|
140
|
+
.pick({ invocationRoot: true })
|
|
141
|
+
.parse(await context.req.json());
|
|
142
|
+
return context.json(await readPrivateLocalSessionDiscovery(input.invocationRoot, {
|
|
143
|
+
signal: context.req.raw.signal,
|
|
144
|
+
}));
|
|
145
|
+
});
|
|
146
|
+
app.get(`${prefix}/agents/:route/sessions/:sessionId/effective-model`, async (context) => {
|
|
147
|
+
const route = [...routes.values()].find((entry) => entry.path === `/agents/${context.req.param("route")}`);
|
|
148
|
+
if (route === undefined)
|
|
149
|
+
return context.body(null, 404);
|
|
150
|
+
return context.json(await route.effectiveModel(context.req.param("sessionId"), context.req.raw.signal));
|
|
151
|
+
});
|
|
152
|
+
app.post(`${prefix}/sessions`, async (context) => {
|
|
153
|
+
const input = sessionCreationSchema.parse(await context.req.json());
|
|
154
|
+
const selected = await owners.createSession(input);
|
|
155
|
+
return context.json(mount(selected, selected.sessionId));
|
|
156
|
+
});
|
|
157
|
+
app.post(`${prefix}/select`, async (context) => {
|
|
158
|
+
const input = selectionSchema.parse(await context.req.json());
|
|
159
|
+
const selected = await owners.select(input);
|
|
160
|
+
return context.json(mount(selected, selected.sessionId));
|
|
161
|
+
});
|
|
162
|
+
app.post(`${prefix}/inspect`, async (context) => {
|
|
163
|
+
const input = sessionInspectionSchema.parse(await context.req.json());
|
|
164
|
+
const selected = await owners.select(input);
|
|
165
|
+
return context.json({
|
|
166
|
+
selected: mount(selected, selected.sessionId),
|
|
167
|
+
snapshot: await selected.session.read({
|
|
168
|
+
limit: input.limit,
|
|
169
|
+
includeSandboxFailures: true,
|
|
170
|
+
}),
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
app.post(`${prefix}/stop`, async (context) => {
|
|
174
|
+
try {
|
|
175
|
+
await shutdown.drain();
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
// Preserve failed owner drain as an observable service failure. The
|
|
179
|
+
// listener remains discoverable for explicit diagnosis.
|
|
180
|
+
return context.json({ error: "Local service resources failed to close" }, 500);
|
|
181
|
+
}
|
|
182
|
+
setTimeout(() => {
|
|
183
|
+
void finish();
|
|
184
|
+
}, 0);
|
|
185
|
+
return context.body(null, 202);
|
|
186
|
+
});
|
|
187
|
+
app.all(`${prefix}/agents/*`, (context) => {
|
|
188
|
+
const path = new URL(context.req.url).pathname;
|
|
189
|
+
for (const route of routes.values()) {
|
|
190
|
+
if (path.startsWith(`${prefix}${route.path}/`))
|
|
191
|
+
return route.fetch(context.req.raw);
|
|
192
|
+
}
|
|
193
|
+
return context.body(null, 404);
|
|
194
|
+
});
|
|
195
|
+
server = openPrivateLocalBunServer((request) => app.fetch(request), undefined, {
|
|
196
|
+
path: `${prefix}/auth`,
|
|
197
|
+
admit: (request) => isLocalRequest(request) && !shutdown.isDraining(),
|
|
198
|
+
createTarget: () => new PrivateLocalAuthTarget(runAuth),
|
|
199
|
+
});
|
|
200
|
+
await writeFileAtomic(recordPath(directory), JSON.stringify({ serviceId, baseUrl: server.baseUrl }), { mode: 0o600 });
|
|
201
|
+
recordWritten = true;
|
|
202
|
+
process.on("SIGINT", onSignal);
|
|
203
|
+
process.on("SIGTERM", onSignal);
|
|
204
|
+
await stopped.promise;
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
process.off("SIGINT", onSignal);
|
|
208
|
+
process.off("SIGTERM", onSignal);
|
|
209
|
+
await finish();
|
|
210
|
+
}
|
|
211
|
+
}
|