@fungi.computer/truffle 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +542 -2
- package/computerd-loopback.cjs +16 -0
- package/dist/auth-cli.d.ts +46 -0
- package/dist/auth-cli.js +373 -0
- package/dist/fungi-account-command.d.ts +18 -0
- package/dist/fungi-account-command.js +95 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +23 -0
- package/dist/internal/image-fit-metafile.json +106 -0
- package/dist/internal/local-image-fit-worker.mjs +960 -0
- package/dist/internal/private-workerd-sandbox-worker.mjs +27487 -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-archive-handoff.d.ts +12 -0
- package/dist/local-archive-handoff.js +21 -0
- package/dist/local-extensions.d.ts +143 -0
- package/dist/local-extensions.js +624 -0
- package/dist/local-image-fit.d.ts +30 -0
- package/dist/local-image-fit.js +105 -0
- package/dist/local-login.d.ts +55 -0
- package/dist/local-login.js +167 -0
- package/dist/local-models.d.ts +16 -0
- package/dist/local-models.js +82 -0
- package/dist/local-session-archives.d.ts +8 -0
- package/dist/local-session-archives.js +152 -0
- package/dist/local-skill-source.d.ts +21 -0
- package/dist/local-skill-source.js +433 -0
- package/dist/log-event.d.ts +9 -0
- package/dist/log-event.js +13 -0
- package/dist/private-bounded-response.d.ts +2 -0
- package/dist/private-bounded-response.js +41 -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 +58 -0
- package/dist/private-file-credential-store.js +331 -0
- package/dist/private-files-patch.d.ts +4 -0
- package/dist/private-files-patch.js +104 -0
- package/dist/private-fungi-account.d.ts +252 -0
- package/dist/private-fungi-account.js +1329 -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 +26 -0
- package/dist/private-fungi-mcp-connection.js +162 -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-schema.d.ts +7 -0
- package/dist/private-fungi-mcp-selection-schema.js +15 -0
- package/dist/private-fungi-mcp-selection.d.ts +14 -0
- package/dist/private-fungi-mcp-selection.js +49 -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 +16 -0
- package/dist/private-fungi-model-catalog.js +86 -0
- package/dist/private-fungi-model-provider.d.ts +6 -0
- package/dist/private-fungi-model-provider.js +51 -0
- package/dist/private-fungi-pending-logouts.d.ts +34 -0
- package/dist/private-fungi-pending-logouts.js +135 -0
- package/dist/private-fungi-service-logout.d.ts +44 -0
- package/dist/private-fungi-service-logout.js +59 -0
- package/dist/private-invocation-computer.d.ts +58 -0
- package/dist/private-invocation-computer.js +498 -0
- package/dist/private-invocation-files.d.ts +30 -0
- package/dist/private-invocation-files.js +520 -0
- package/dist/private-local-acp-stdio.d.ts +17 -0
- package/dist/private-local-acp-stdio.js +145 -0
- package/dist/private-local-admission-gate.d.ts +14 -0
- package/dist/private-local-admission-gate.js +52 -0
- package/dist/private-local-agent-control.d.ts +101 -0
- package/dist/private-local-agent-control.js +30 -0
- package/dist/private-local-agent-default-model.d.ts +12 -0
- package/dist/private-local-agent-default-model.js +31 -0
- package/dist/private-local-auth-rpc.d.ts +129 -0
- package/dist/private-local-auth-rpc.js +396 -0
- package/dist/private-local-bwrap.d.ts +13 -0
- package/dist/private-local-bwrap.js +70 -0
- package/dist/private-local-client-version.d.ts +5 -0
- package/dist/private-local-client-version.js +43 -0
- package/dist/private-local-computer-attach.d.ts +49 -0
- package/dist/private-local-computer-attach.js +230 -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 +202 -0
- package/dist/private-local-computer-processes.js +613 -0
- package/dist/private-local-config-root.d.ts +21 -0
- package/dist/private-local-config-root.js +19 -0
- package/dist/private-local-containment.d.ts +51 -0
- package/dist/private-local-containment.js +161 -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 +36 -0
- package/dist/private-local-delegation.js +133 -0
- package/dist/private-local-effective-model.d.ts +27 -0
- package/dist/private-local-effective-model.js +17 -0
- package/dist/private-local-exec-environment.d.ts +16 -0
- package/dist/private-local-exec-environment.js +11 -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-linux-sandbox.d.ts +8 -0
- package/dist/private-local-linux-sandbox.js +67 -0
- package/dist/private-local-mail.d.ts +13 -0
- package/dist/private-local-mail.js +77 -0
- package/dist/private-local-mesh-enrollment.d.ts +34 -0
- package/dist/private-local-mesh-enrollment.js +188 -0
- package/dist/private-local-mesh-links.d.ts +21 -0
- package/dist/private-local-mesh-links.js +229 -0
- package/dist/private-local-mesh-record.d.ts +120 -0
- package/dist/private-local-mesh-record.js +55 -0
- package/dist/private-local-process-identity.d.ts +20 -0
- package/dist/private-local-process-identity.js +40 -0
- package/dist/private-local-runtime-host.d.ts +192 -0
- package/dist/private-local-runtime-host.js +1177 -0
- package/dist/private-local-sandbox-policy.d.ts +21 -0
- package/dist/private-local-sandbox-policy.js +121 -0
- package/dist/private-local-sandbox.d.ts +37 -0
- package/dist/private-local-sandbox.js +114 -0
- package/dist/private-local-seatbelt.d.ts +24 -0
- package/dist/private-local-seatbelt.js +68 -0
- package/dist/private-local-service-contract.d.ts +170 -0
- package/dist/private-local-service-contract.js +1 -0
- package/dist/private-local-service-discovery.d.ts +217 -0
- package/dist/private-local-service-discovery.js +322 -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-lease.d.ts +16 -0
- package/dist/private-local-service-lease.js +105 -0
- package/dist/private-local-service-listener.d.ts +22 -0
- package/dist/private-local-service-listener.js +407 -0
- package/dist/private-local-service-log.d.ts +128 -0
- package/dist/private-local-service-log.js +321 -0
- package/dist/private-local-service-owner.d.ts +135 -0
- package/dist/private-local-service-owner.js +1369 -0
- package/dist/private-local-service-process.d.ts +1 -0
- package/dist/private-local-service-process.js +8 -0
- package/dist/private-local-service-termination.d.ts +7 -0
- package/dist/private-local-service-termination.js +67 -0
- package/dist/private-local-service.d.ts +46 -0
- package/dist/private-local-service.js +606 -0
- package/dist/private-local-session-discovery.d.ts +66 -0
- package/dist/private-local-session-discovery.js +278 -0
- package/dist/private-local-shiitake-host.d.ts +44 -0
- package/dist/private-local-shiitake-host.js +143 -0
- package/dist/private-local-startup-model.d.ts +51 -0
- package/dist/private-local-startup-model.js +86 -0
- package/dist/private-local-stop-all.d.ts +27 -0
- package/dist/private-local-stop-all.js +26 -0
- package/dist/private-local-ui-attachment.d.ts +19 -0
- package/dist/private-local-ui-attachment.js +71 -0
- package/dist/private-local-worker-server.d.ts +16 -0
- package/dist/private-local-worker-server.js +144 -0
- package/dist/private-mcp-form-contract.d.ts +186 -0
- package/dist/private-mcp-form-contract.js +53 -0
- package/dist/private-mcp-form-elicitation.d.ts +18 -0
- package/dist/private-mcp-form-elicitation.js +97 -0
- package/dist/private-mcp-form-session.d.ts +172 -0
- package/dist/private-mcp-form-session.js +148 -0
- package/dist/private-openai-claims.d.ts +30 -0
- package/dist/private-openai-claims.js +69 -0
- package/dist/private-oyster-mcp-auth.d.ts +13 -0
- package/dist/private-oyster-mcp-auth.js +57 -0
- package/dist/private-provider-rejection.d.ts +15 -0
- package/dist/private-provider-rejection.js +24 -0
- package/dist/private-sqlite-owner.d.ts +39 -0
- package/dist/private-sqlite-owner.js +169 -0
- package/dist/private-team-computers.d.ts +38 -0
- package/dist/private-team-computers.js +110 -0
- package/dist/private-workerd-sandbox.d.ts +3 -0
- package/dist/private-workerd-sandbox.js +347 -0
- package/dist/seatbelt/safe-base.sbpl +79 -0
- package/dist/subscription-activation.d.ts +9 -0
- package/dist/subscription-activation.js +62 -0
- package/dist/subscription-identity.d.ts +23 -0
- package/dist/subscription-identity.js +124 -0
- package/dist/subscription-turn.d.ts +17 -0
- package/dist/subscription-turn.js +143 -0
- package/dist/team-apps.d.ts +24 -0
- package/dist/team-apps.js +128 -0
- package/dist/tracker-git-credential.d.ts +15 -0
- package/dist/tracker-git-credential.js +87 -0
- package/native/bin/LICENSE.bubblewrap +502 -0
- package/native/bin/LICENSE.libcap +112 -0
- package/native/bin/LICENSE.sandbox-runtime +201 -0
- package/native/bin/SOURCE.bubblewrap.tar.xz +0 -0
- package/native/bin/build.json +4037 -0
- package/native/bin/bwrap-build-source/Dockerfile +48 -0
- package/native/bin/bwrap-build-source/README.md +96 -0
- package/native/bin/bwrap-build-source/build.mjs +52 -0
- package/native/bin/bwrap-build-source/linux-arm64.ini +14 -0
- package/native/bin/bwrap-build-source/pin.mjs +21 -0
- package/native/bin/bwrap-build-source/record.mjs +74 -0
- package/native/bin/bwrap-build-source/vendor/LICENSE.sandbox-runtime +201 -0
- package/native/bin/bwrap-build-source/vendor/README.md +23 -0
- package/native/bin/bwrap-build-source/vendor/seccomp-unix-block.c +280 -0
- package/native/bin/bwrap-build.json +44 -0
- package/native/bin/fungi-apply-patch +0 -0
- package/native/bin/fungi-bwrap +0 -0
- package/native/bin/fungi-bwrap.sha256 +1 -0
- package/native/bin/fungi-seccomp.bpf +0 -0
- package/native/bin/fungi-seccomp.bpf.sha256 +1 -0
- package/package.json +161 -3
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export { sandboxStatusSchema } from "./private-local-sandbox.js";
|
|
3
|
+
/**
|
|
4
|
+
* The client-service wire contract. Bump it with any change an older client
|
|
5
|
+
* cannot read; a running client then asks its window to restart.
|
|
6
|
+
*/
|
|
7
|
+
export declare const privateLocalServiceProtocol = 3;
|
|
8
|
+
/** This build's identity: the packed CLI's content hash, or `source` for a source run. */
|
|
9
|
+
export declare function privateLocalServiceVersion(): string;
|
|
10
|
+
/**
|
|
11
|
+
* The fields every protocol keeps in `service.json`, so any client can
|
|
12
|
+
* identify, replace or stop a service whatever protocol it speaks.
|
|
13
|
+
*/
|
|
14
|
+
declare const serviceLifecycleSchema: z.ZodObject<{
|
|
15
|
+
protocol: z.ZodNumber;
|
|
16
|
+
version: z.ZodString;
|
|
17
|
+
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>>;
|
|
18
|
+
pid: z.ZodNumber;
|
|
19
|
+
startedAt: z.ZodString;
|
|
20
|
+
}, z.core.$loose>;
|
|
21
|
+
declare const serviceRecordSchema: z.ZodObject<{
|
|
22
|
+
protocol: z.ZodLiteral<3>;
|
|
23
|
+
baseUrl: z.ZodURL;
|
|
24
|
+
version: z.ZodString;
|
|
25
|
+
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>>;
|
|
26
|
+
pid: z.ZodNumber;
|
|
27
|
+
startedAt: z.ZodString;
|
|
28
|
+
}, z.core.$strict>;
|
|
29
|
+
export type ServiceRecord = z.infer<typeof serviceRecordSchema>;
|
|
30
|
+
/** Mandatory live diagnostics; never written into the registration pointer. */
|
|
31
|
+
export declare const liveServiceStatusSchema: z.ZodObject<{
|
|
32
|
+
protocol: z.ZodLiteral<3>;
|
|
33
|
+
baseUrl: z.ZodURL;
|
|
34
|
+
version: z.ZodString;
|
|
35
|
+
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>>;
|
|
36
|
+
pid: z.ZodNumber;
|
|
37
|
+
startedAt: z.ZodString;
|
|
38
|
+
diagnostics: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
39
|
+
state: z.ZodLiteral<"active">;
|
|
40
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
41
|
+
state: z.ZodLiteral<"failed">;
|
|
42
|
+
code: z.ZodEnum<{
|
|
43
|
+
"sink-open-failed": "sink-open-failed";
|
|
44
|
+
"sink-write-failed": "sink-write-failed";
|
|
45
|
+
"sink-close-failed": "sink-close-failed";
|
|
46
|
+
}>;
|
|
47
|
+
since: z.ZodISODateTime;
|
|
48
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
49
|
+
state: z.ZodLiteral<"recovery-disabled">;
|
|
50
|
+
code: z.ZodEnum<{
|
|
51
|
+
"read-failed": "read-failed";
|
|
52
|
+
"decode-failed": "decode-failed";
|
|
53
|
+
}>;
|
|
54
|
+
since: z.ZodISODateTime;
|
|
55
|
+
}, z.core.$strict>], "state">;
|
|
56
|
+
}, z.core.$strict>;
|
|
57
|
+
export type LiveServiceStatus = z.infer<typeof liveServiceStatusSchema>;
|
|
58
|
+
/**
|
|
59
|
+
* What `service.json` names: a service of this protocol, a service of another
|
|
60
|
+
* protocol (identified only by its lifecycle fields), or a file no protocol
|
|
61
|
+
* wrote.
|
|
62
|
+
*/
|
|
63
|
+
export type ServiceRegistration = Readonly<{
|
|
64
|
+
kind: "current";
|
|
65
|
+
record: ServiceRecord;
|
|
66
|
+
}> | Readonly<{
|
|
67
|
+
kind: "other-protocol";
|
|
68
|
+
lifecycle: z.infer<typeof serviceLifecycleSchema>;
|
|
69
|
+
}> | Readonly<{
|
|
70
|
+
kind: "unidentified";
|
|
71
|
+
}>;
|
|
72
|
+
export declare const selectionSchema: z.ZodObject<{
|
|
73
|
+
uiAttachmentLifetime: z.ZodOptional<z.ZodEnum<{
|
|
74
|
+
watch: "watch";
|
|
75
|
+
connection: "connection";
|
|
76
|
+
}>>;
|
|
77
|
+
invocationRoot: z.ZodString;
|
|
78
|
+
clientPath: z.ZodString;
|
|
79
|
+
requireSandbox: z.ZodOptional<z.ZodBoolean>;
|
|
80
|
+
databasePath: z.ZodOptional<z.ZodString>;
|
|
81
|
+
sessionId: z.ZodOptional<z.ZodString>;
|
|
82
|
+
fungiMcp: z.ZodOptional<z.ZodObject<{
|
|
83
|
+
endpoint: z.ZodPipe<z.ZodURL, z.ZodString>;
|
|
84
|
+
authentication: z.ZodOptional<z.ZodLiteral<"oyster-key">>;
|
|
85
|
+
}, z.core.$strict>>;
|
|
86
|
+
}, z.core.$strict>;
|
|
87
|
+
export declare const sessionCreationSchema: z.ZodObject<{
|
|
88
|
+
uiAttachmentLifetime: z.ZodOptional<z.ZodEnum<{
|
|
89
|
+
watch: "watch";
|
|
90
|
+
connection: "connection";
|
|
91
|
+
}>>;
|
|
92
|
+
invocationRoot: z.ZodString;
|
|
93
|
+
clientPath: z.ZodString;
|
|
94
|
+
requireSandbox: z.ZodOptional<z.ZodBoolean>;
|
|
95
|
+
databasePath: z.ZodString;
|
|
96
|
+
requestId: z.ZodString;
|
|
97
|
+
fungiMcp: z.ZodOptional<z.ZodObject<{
|
|
98
|
+
endpoint: z.ZodPipe<z.ZodURL, z.ZodString>;
|
|
99
|
+
authentication: z.ZodOptional<z.ZodLiteral<"oyster-key">>;
|
|
100
|
+
}, z.core.$strict>>;
|
|
101
|
+
}, z.core.$strict>;
|
|
102
|
+
export declare const sessionInspectionSchema: z.ZodObject<{
|
|
103
|
+
databasePath: z.ZodOptional<z.ZodString>;
|
|
104
|
+
invocationRoot: z.ZodString;
|
|
105
|
+
fungiMcp: z.ZodOptional<z.ZodObject<{
|
|
106
|
+
endpoint: z.ZodPipe<z.ZodURL, z.ZodString>;
|
|
107
|
+
authentication: z.ZodOptional<z.ZodLiteral<"oyster-key">>;
|
|
108
|
+
}, z.core.$strict>>;
|
|
109
|
+
requireSandbox: z.ZodOptional<z.ZodBoolean>;
|
|
110
|
+
sessionId: z.ZodString;
|
|
111
|
+
limit: z.ZodNumber;
|
|
112
|
+
}, z.core.$strict>;
|
|
113
|
+
export declare const selectedSchema: z.ZodObject<{
|
|
114
|
+
agentId: z.ZodString;
|
|
115
|
+
databasePath: z.ZodString;
|
|
116
|
+
sessionId: z.ZodString;
|
|
117
|
+
routePath: z.ZodTemplateLiteral<`/agents/${string}`>;
|
|
118
|
+
}, z.core.$strict>;
|
|
119
|
+
export declare const discoverRequestSchema: z.ZodObject<{
|
|
120
|
+
invocationRoot: z.ZodString;
|
|
121
|
+
archived: z.ZodDefault<z.ZodBoolean>;
|
|
122
|
+
}, z.core.$strict>;
|
|
123
|
+
export declare const archiveRequestSchema: z.ZodObject<{
|
|
124
|
+
invocationRoot: z.ZodString;
|
|
125
|
+
agentId: z.ZodString;
|
|
126
|
+
archived: z.ZodBoolean;
|
|
127
|
+
}, z.core.$strict>;
|
|
128
|
+
/** The 409 answer to an archive the Local service refuses. */
|
|
129
|
+
export declare const archiveRefusalSchema: z.ZodObject<{
|
|
130
|
+
code: z.ZodLiteral<"agent_archive_refused">;
|
|
131
|
+
reason: z.ZodEnum<{
|
|
132
|
+
pending: "pending";
|
|
133
|
+
running: "running";
|
|
134
|
+
archived: "archived";
|
|
135
|
+
not_creator: "not_creator";
|
|
136
|
+
self: "self";
|
|
137
|
+
unreadable: "unreadable";
|
|
138
|
+
}>;
|
|
139
|
+
error: z.ZodString;
|
|
140
|
+
}, z.core.$strict>;
|
|
141
|
+
export declare const sessionDiscoverySchema: z.ZodObject<{
|
|
142
|
+
root: z.ZodString;
|
|
143
|
+
agents: z.ZodArray<z.ZodObject<{
|
|
144
|
+
agentId: z.ZodString;
|
|
145
|
+
name: z.ZodOptional<z.ZodString>;
|
|
146
|
+
createdBy: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
147
|
+
unreadable: z.ZodOptional<z.ZodString>;
|
|
148
|
+
databasePath: z.ZodString;
|
|
149
|
+
sessionCount: z.ZodNumber;
|
|
150
|
+
updatedAt: z.ZodOptional<z.ZodNumber>;
|
|
151
|
+
}, z.core.$strict>>;
|
|
152
|
+
sessions: z.ZodArray<z.ZodObject<{
|
|
153
|
+
databasePath: z.ZodString;
|
|
154
|
+
sessionId: z.ZodString;
|
|
155
|
+
title: z.ZodString;
|
|
156
|
+
createdAt: z.ZodNumber;
|
|
157
|
+
updatedAt: z.ZodNumber;
|
|
158
|
+
}, z.core.$strict>>;
|
|
159
|
+
problems: z.ZodArray<z.ZodObject<{
|
|
160
|
+
kind: z.ZodEnum<{
|
|
161
|
+
corrupt: "corrupt";
|
|
162
|
+
unreadable: "unreadable";
|
|
163
|
+
in_use: "in_use";
|
|
164
|
+
}>;
|
|
165
|
+
}, z.core.$strict>>;
|
|
166
|
+
}, z.core.$strict>;
|
|
167
|
+
export declare const modelChoicesSchema: z.ZodArray<z.ZodObject<{
|
|
168
|
+
providerId: z.ZodString;
|
|
169
|
+
providerName: z.ZodString;
|
|
170
|
+
id: z.ZodString;
|
|
171
|
+
name: z.ZodString;
|
|
172
|
+
reasoning: z.ZodArray<z.ZodEnum<{
|
|
173
|
+
minimal: "minimal";
|
|
174
|
+
low: "low";
|
|
175
|
+
medium: "medium";
|
|
176
|
+
high: "high";
|
|
177
|
+
xhigh: "xhigh";
|
|
178
|
+
max: "max";
|
|
179
|
+
}>>;
|
|
180
|
+
}, z.core.$strict>>;
|
|
181
|
+
/** Resolve a read target without creating/chmodding a directory. */
|
|
182
|
+
export declare function serviceDirectoryPath(directory?: string): string;
|
|
183
|
+
export declare function serviceDirectory(directory?: string): Promise<string>;
|
|
184
|
+
export declare const recordPath: (directory: string) => string;
|
|
185
|
+
/** Read `service.json` by its lifecycle fields first, so any protocol's record is named. */
|
|
186
|
+
export declare function readServiceRegistration(directory: string): Promise<ServiceRegistration | undefined>;
|
|
187
|
+
/** The record of a service speaking this client's protocol, if one is registered. */
|
|
188
|
+
export declare function readServiceRecord(directory: string): Promise<ServiceRecord | undefined>;
|
|
189
|
+
/** An older listener must not erase its successor's discovery record. */
|
|
190
|
+
export declare function removeOwnedServiceRecord(directory: string, serviceId: string): Promise<void>;
|
|
191
|
+
/** Remove a `service.json` no protocol wrote; only call it once no owner is live. */
|
|
192
|
+
export declare function removeUnidentifiedServiceRecord(directory: string): Promise<void>;
|
|
193
|
+
/** Legacy owners have no SQLite lease, so their file lock must not be stolen. */
|
|
194
|
+
export declare function assertNoUntrackedServiceOwner(directory: string): Promise<void>;
|
|
195
|
+
/** Startup and shutdown serialize with the native listener and lifetime owner lock. */
|
|
196
|
+
export declare function acquireServiceLock(directory: string, name: "startup" | "owner"): Promise<() => Promise<void>>;
|
|
197
|
+
/**
|
|
198
|
+
* Whether a service process holds this directory's lifetime lease. A held
|
|
199
|
+
* lease is a live owner even when it is too busy to answer its handshake.
|
|
200
|
+
*/
|
|
201
|
+
export declare function serviceOwnerIsLive(directory: string): Promise<boolean>;
|
|
202
|
+
export type ServiceProbe = Readonly<{
|
|
203
|
+
kind: "running";
|
|
204
|
+
service: LiveServiceStatus;
|
|
205
|
+
}> | Readonly<{
|
|
206
|
+
kind: "not-running";
|
|
207
|
+
}> | Readonly<{
|
|
208
|
+
kind: "unavailable";
|
|
209
|
+
}>;
|
|
210
|
+
export declare function readLiveServiceStatus(record: ServiceRecord, timeoutMs?: number): Promise<ServiceProbe>;
|
|
211
|
+
export declare function handshake(record: ServiceRecord): Promise<boolean>;
|
|
212
|
+
/**
|
|
213
|
+
* Clear the lifetime lock a killed owner left behind. The lease's process is
|
|
214
|
+
* gone, so acquiring the owner lock removes the marked directory;
|
|
215
|
+
* releasing it at once leaves nothing behind. A live owner makes this throw.
|
|
216
|
+
*/
|
|
217
|
+
export declare function clearAbandonedServiceOwner(directory: string): Promise<void>;
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { fungiMcpSelectionSchema } from "./private-fungi-mcp-selection-schema.js";
|
|
2
|
+
import { localUiAttachmentLifetimeSchema } from "./private-local-ui-attachment.js";
|
|
3
|
+
import { privateLocalExecPathSchema } from "./private-local-exec-path.js";
|
|
4
|
+
import { chmod, mkdir, readFile, realpath, rm, rmdir, stat, writeFile, } from "node:fs/promises";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import lockfile from "proper-lockfile";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { localServiceIds } from "./private-local-service-ids.js";
|
|
10
|
+
import { acquireServiceOwnerLease, readServiceLeaseOwner, } from "./private-local-service-lease.js";
|
|
11
|
+
import { modelReasoningLevels } from "@fungi.computer/shiitake/model-preference";
|
|
12
|
+
import { archiveRefusalReasons } from "@fungi.computer/shiitake/agent-control";
|
|
13
|
+
import { serviceDiagnosticsStatusSchema } from "./private-local-service-log.js";
|
|
14
|
+
export { sandboxStatusSchema } from "./private-local-sandbox.js";
|
|
15
|
+
/**
|
|
16
|
+
* The client-service wire contract. Bump it with any change an older client
|
|
17
|
+
* cannot read; a running client then asks its window to restart.
|
|
18
|
+
*/
|
|
19
|
+
export const privateLocalServiceProtocol = 3;
|
|
20
|
+
/** This build's identity: the packed CLI's content hash, or `source` for a source run. */
|
|
21
|
+
export function privateLocalServiceVersion() {
|
|
22
|
+
return (z
|
|
23
|
+
.string()
|
|
24
|
+
.regex(/^[a-f0-9]{64}$/u)
|
|
25
|
+
.optional()
|
|
26
|
+
.parse(process.env.FUNGI_BUNDLED_BUILD_ID) ?? "source");
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The fields every protocol keeps in `service.json`, so any client can
|
|
30
|
+
* identify, replace or stop a service whatever protocol it speaks.
|
|
31
|
+
*/
|
|
32
|
+
const serviceLifecycleSchema = z.looseObject({
|
|
33
|
+
protocol: z.number().int().positive(),
|
|
34
|
+
version: z.string().min(1),
|
|
35
|
+
serviceId: z.string().refine((value) => localServiceIds.service.is(value)),
|
|
36
|
+
/** The listener process. */
|
|
37
|
+
pid: z.number().int().positive(),
|
|
38
|
+
/**
|
|
39
|
+
* Pointer to the owner identity; signal authority comes from the lease row,
|
|
40
|
+
* not these file fields.
|
|
41
|
+
*/
|
|
42
|
+
startedAt: z.string().min(1),
|
|
43
|
+
});
|
|
44
|
+
const serviceRecordSchema = z.strictObject({
|
|
45
|
+
...serviceLifecycleSchema.shape,
|
|
46
|
+
protocol: z.literal(privateLocalServiceProtocol),
|
|
47
|
+
baseUrl: z.url().refine((value) => {
|
|
48
|
+
const url = new URL(value);
|
|
49
|
+
return (url.protocol === "http:" &&
|
|
50
|
+
url.hostname === "127.0.0.1" &&
|
|
51
|
+
url.port !== "" &&
|
|
52
|
+
url.username === "" &&
|
|
53
|
+
url.password === "" &&
|
|
54
|
+
url.pathname === "/" &&
|
|
55
|
+
url.search === "" &&
|
|
56
|
+
url.hash === "");
|
|
57
|
+
}),
|
|
58
|
+
});
|
|
59
|
+
/** Mandatory live diagnostics; never written into the registration pointer. */
|
|
60
|
+
export const liveServiceStatusSchema = serviceRecordSchema.extend({
|
|
61
|
+
diagnostics: serviceDiagnosticsStatusSchema,
|
|
62
|
+
});
|
|
63
|
+
export const selectionSchema = z.strictObject({
|
|
64
|
+
uiAttachmentLifetime: localUiAttachmentLifetimeSchema.optional(),
|
|
65
|
+
invocationRoot: z.string().min(1),
|
|
66
|
+
clientPath: privateLocalExecPathSchema,
|
|
67
|
+
requireSandbox: z.boolean().optional(),
|
|
68
|
+
databasePath: z.string().min(1).optional(),
|
|
69
|
+
sessionId: z.string().min(1).optional(),
|
|
70
|
+
fungiMcp: fungiMcpSelectionSchema.optional(),
|
|
71
|
+
});
|
|
72
|
+
export const sessionCreationSchema = z.strictObject({
|
|
73
|
+
uiAttachmentLifetime: localUiAttachmentLifetimeSchema.optional(),
|
|
74
|
+
invocationRoot: z.string().min(1),
|
|
75
|
+
clientPath: privateLocalExecPathSchema,
|
|
76
|
+
requireSandbox: z.boolean().optional(),
|
|
77
|
+
databasePath: z.string().min(1),
|
|
78
|
+
requestId: z.string().min(1),
|
|
79
|
+
fungiMcp: fungiMcpSelectionSchema.optional(),
|
|
80
|
+
});
|
|
81
|
+
export const sessionInspectionSchema = selectionSchema
|
|
82
|
+
.omit({ clientPath: true, uiAttachmentLifetime: true })
|
|
83
|
+
.extend({
|
|
84
|
+
sessionId: z.string().min(1),
|
|
85
|
+
limit: z.number().int().min(1).max(200),
|
|
86
|
+
});
|
|
87
|
+
export const selectedSchema = z.strictObject({
|
|
88
|
+
agentId: z.string().min(1),
|
|
89
|
+
databasePath: z.string().min(1),
|
|
90
|
+
sessionId: z.string().min(1),
|
|
91
|
+
routePath: z.templateLiteral(["/agents/", z.uuid({ version: "v4" })]),
|
|
92
|
+
});
|
|
93
|
+
export const discoverRequestSchema = z.strictObject({
|
|
94
|
+
invocationRoot: z.string().min(1),
|
|
95
|
+
archived: z.boolean().default(false),
|
|
96
|
+
});
|
|
97
|
+
export const archiveRequestSchema = z.strictObject({
|
|
98
|
+
invocationRoot: z.string().min(1),
|
|
99
|
+
agentId: z.string().min(1),
|
|
100
|
+
archived: z.boolean(),
|
|
101
|
+
});
|
|
102
|
+
/** The 409 answer to an archive the Local service refuses. */
|
|
103
|
+
export const archiveRefusalSchema = z.strictObject({
|
|
104
|
+
code: z.literal("agent_archive_refused"),
|
|
105
|
+
reason: z.enum(archiveRefusalReasons),
|
|
106
|
+
error: z.string(),
|
|
107
|
+
});
|
|
108
|
+
export const sessionDiscoverySchema = z.strictObject({
|
|
109
|
+
root: z.string().min(1),
|
|
110
|
+
agents: z.array(z.strictObject({
|
|
111
|
+
agentId: z.string().min(1),
|
|
112
|
+
name: z.string().optional(),
|
|
113
|
+
createdBy: z.string().nullable().optional(),
|
|
114
|
+
unreadable: z.string().optional(),
|
|
115
|
+
databasePath: z.string().min(1),
|
|
116
|
+
sessionCount: z.number().int().nonnegative(),
|
|
117
|
+
updatedAt: z.number().optional(),
|
|
118
|
+
})),
|
|
119
|
+
sessions: z.array(z.strictObject({
|
|
120
|
+
databasePath: z.string().min(1),
|
|
121
|
+
sessionId: z.string().min(1),
|
|
122
|
+
title: z.string(),
|
|
123
|
+
createdAt: z.number(),
|
|
124
|
+
updatedAt: z.number(),
|
|
125
|
+
})),
|
|
126
|
+
problems: z.array(z.strictObject({ kind: z.enum(["corrupt", "in_use", "unreadable"]) })),
|
|
127
|
+
});
|
|
128
|
+
export const modelChoicesSchema = z.array(z.strictObject({
|
|
129
|
+
providerId: z.string().min(1),
|
|
130
|
+
providerName: z.string().min(1),
|
|
131
|
+
id: z.string().min(1),
|
|
132
|
+
name: z.string().min(1),
|
|
133
|
+
reasoning: z.array(z.enum(modelReasoningLevels)),
|
|
134
|
+
}));
|
|
135
|
+
/** Resolve a read target without creating/chmodding a directory. */
|
|
136
|
+
export function serviceDirectoryPath(directory) {
|
|
137
|
+
return resolve(directory ?? join(homedir(), ".fungi", "service"));
|
|
138
|
+
}
|
|
139
|
+
export async function serviceDirectory(directory) {
|
|
140
|
+
const path = serviceDirectoryPath(directory);
|
|
141
|
+
await mkdir(path, { recursive: true, mode: 0o700 });
|
|
142
|
+
await chmod(path, 0o700);
|
|
143
|
+
return realpath(path);
|
|
144
|
+
}
|
|
145
|
+
export const recordPath = (directory) => join(directory, "service.json");
|
|
146
|
+
/** Read `service.json` by its lifecycle fields first, so any protocol's record is named. */
|
|
147
|
+
export async function readServiceRegistration(directory) {
|
|
148
|
+
let text;
|
|
149
|
+
try {
|
|
150
|
+
text = await readFile(recordPath(directory), "utf8");
|
|
151
|
+
}
|
|
152
|
+
catch (cause) {
|
|
153
|
+
if (cause instanceof Error && "code" in cause && cause.code === "ENOENT")
|
|
154
|
+
return undefined;
|
|
155
|
+
throw cause;
|
|
156
|
+
}
|
|
157
|
+
let json;
|
|
158
|
+
try {
|
|
159
|
+
json = JSON.parse(text);
|
|
160
|
+
}
|
|
161
|
+
catch (cause) {
|
|
162
|
+
if (cause instanceof SyntaxError)
|
|
163
|
+
return { kind: "unidentified" };
|
|
164
|
+
throw cause;
|
|
165
|
+
}
|
|
166
|
+
const lifecycle = serviceLifecycleSchema.safeParse(json);
|
|
167
|
+
if (!lifecycle.success) {
|
|
168
|
+
// A service from before the PID was recorded cannot be signalled; name it
|
|
169
|
+
// and its own stop route instead of treating it as unidentified.
|
|
170
|
+
const older = serviceRecordSchema
|
|
171
|
+
.pick({ serviceId: true, baseUrl: true })
|
|
172
|
+
.safeParse(json);
|
|
173
|
+
if (older.success)
|
|
174
|
+
throw new LocalServiceRecordWithoutPid(directory, older.data);
|
|
175
|
+
return { kind: "unidentified" };
|
|
176
|
+
}
|
|
177
|
+
if (lifecycle.data.protocol !== privateLocalServiceProtocol)
|
|
178
|
+
return { kind: "other-protocol", lifecycle: lifecycle.data };
|
|
179
|
+
return { kind: "current", record: serviceRecordSchema.parse(json) };
|
|
180
|
+
}
|
|
181
|
+
/** `service.json` was written by a service too old to record its PID. */
|
|
182
|
+
class LocalServiceRecordWithoutPid extends Error {
|
|
183
|
+
name = "LocalServiceRecordWithoutPid";
|
|
184
|
+
constructor(directory, service) {
|
|
185
|
+
const stopUrl = new URL(`/service/${service.serviceId}/stop`, service.baseUrl);
|
|
186
|
+
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.`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/** The record of a service speaking this client's protocol, if one is registered. */
|
|
190
|
+
export async function readServiceRecord(directory) {
|
|
191
|
+
const registration = await readServiceRegistration(directory);
|
|
192
|
+
return registration?.kind === "current" ? registration.record : undefined;
|
|
193
|
+
}
|
|
194
|
+
/** An older listener must not erase its successor's discovery record. */
|
|
195
|
+
export async function removeOwnedServiceRecord(directory, serviceId) {
|
|
196
|
+
const registration = await readServiceRegistration(directory);
|
|
197
|
+
if ((registration?.kind === "current" &&
|
|
198
|
+
registration.record.serviceId === serviceId) ||
|
|
199
|
+
(registration?.kind === "other-protocol" &&
|
|
200
|
+
registration.lifecycle.serviceId === serviceId))
|
|
201
|
+
await rm(recordPath(directory), { force: true });
|
|
202
|
+
}
|
|
203
|
+
/** Remove a `service.json` no protocol wrote; only call it once no owner is live. */
|
|
204
|
+
export async function removeUnidentifiedServiceRecord(directory) {
|
|
205
|
+
if ((await readServiceRegistration(directory))?.kind === "unidentified")
|
|
206
|
+
await rm(recordPath(directory), { force: true });
|
|
207
|
+
}
|
|
208
|
+
const ownerLeaseMarker = (directory) => join(directory, "owner.lock", "sqlite-owner");
|
|
209
|
+
async function pathExists(path) {
|
|
210
|
+
try {
|
|
211
|
+
await stat(path);
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
catch (cause) {
|
|
215
|
+
if (cause instanceof Error && "code" in cause && cause.code === "ENOENT")
|
|
216
|
+
return false;
|
|
217
|
+
throw cause;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/** Legacy owners have no SQLite lease, so their file lock must not be stolen. */
|
|
221
|
+
export async function assertNoUntrackedServiceOwner(directory) {
|
|
222
|
+
if ((await pathExists(join(directory, "owner.lock"))) &&
|
|
223
|
+
!(await pathExists(ownerLeaseMarker(directory))))
|
|
224
|
+
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.");
|
|
225
|
+
}
|
|
226
|
+
/** Startup and shutdown serialize with the native listener and lifetime owner lock. */
|
|
227
|
+
export async function acquireServiceLock(directory, name) {
|
|
228
|
+
if (name === "startup")
|
|
229
|
+
return lockfile.lock(join(directory, name), {
|
|
230
|
+
realpath: false,
|
|
231
|
+
lockfilePath: join(directory, `${name}.lock`),
|
|
232
|
+
retries: { retries: 100, minTimeout: 100, maxTimeout: 100 },
|
|
233
|
+
onCompromised(error) {
|
|
234
|
+
throw error;
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
const releaseLease = await acquireServiceOwnerLease(directory);
|
|
238
|
+
const lockPath = join(directory, "owner.lock");
|
|
239
|
+
let created = false;
|
|
240
|
+
try {
|
|
241
|
+
await assertNoUntrackedServiceOwner(directory);
|
|
242
|
+
// A dead owner can leave a marked directory behind. Its process claim has
|
|
243
|
+
// already been replaced transactionally, without trusting a heartbeat.
|
|
244
|
+
if (await pathExists(ownerLeaseMarker(directory)))
|
|
245
|
+
await rm(lockPath, { recursive: true });
|
|
246
|
+
await mkdir(lockPath);
|
|
247
|
+
created = true;
|
|
248
|
+
await writeFile(ownerLeaseMarker(directory), "", {
|
|
249
|
+
flag: "wx",
|
|
250
|
+
mode: 0o600,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
catch (cause) {
|
|
254
|
+
try {
|
|
255
|
+
if (created)
|
|
256
|
+
await rm(lockPath, { recursive: true });
|
|
257
|
+
}
|
|
258
|
+
finally {
|
|
259
|
+
releaseLease();
|
|
260
|
+
}
|
|
261
|
+
throw cause;
|
|
262
|
+
}
|
|
263
|
+
return async () => {
|
|
264
|
+
try {
|
|
265
|
+
await rm(ownerLeaseMarker(directory), { force: true });
|
|
266
|
+
await rmdir(lockPath);
|
|
267
|
+
}
|
|
268
|
+
finally {
|
|
269
|
+
releaseLease();
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Whether a service process holds this directory's lifetime lease. A held
|
|
275
|
+
* lease is a live owner even when it is too busy to answer its handshake.
|
|
276
|
+
*/
|
|
277
|
+
export async function serviceOwnerIsLive(directory) {
|
|
278
|
+
return (await readServiceLeaseOwner(directory)) !== undefined;
|
|
279
|
+
}
|
|
280
|
+
const transportFailureSchema = z.looseObject({
|
|
281
|
+
code: z.string().optional(),
|
|
282
|
+
cause: z.looseObject({ code: z.string().optional() }).optional(),
|
|
283
|
+
});
|
|
284
|
+
export async function readLiveServiceStatus(record, timeoutMs = 2000) {
|
|
285
|
+
try {
|
|
286
|
+
const response = await fetch(`${record.baseUrl}/service/${record.serviceId}/status`, {
|
|
287
|
+
redirect: "error",
|
|
288
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
289
|
+
});
|
|
290
|
+
if (!response.ok)
|
|
291
|
+
return { kind: "unavailable" };
|
|
292
|
+
const actual = liveServiceStatusSchema.parse(await response.json());
|
|
293
|
+
return actual.serviceId === record.serviceId &&
|
|
294
|
+
actual.baseUrl === record.baseUrl &&
|
|
295
|
+
actual.pid === record.pid &&
|
|
296
|
+
actual.startedAt === record.startedAt &&
|
|
297
|
+
actual.version === record.version
|
|
298
|
+
? { kind: "running", service: actual }
|
|
299
|
+
: { kind: "unavailable" };
|
|
300
|
+
}
|
|
301
|
+
catch (cause) {
|
|
302
|
+
const failure = transportFailureSchema.safeParse(cause);
|
|
303
|
+
if (failure.success) {
|
|
304
|
+
const codes = [failure.data.code, failure.data.cause?.code];
|
|
305
|
+
if (codes.includes("ECONNREFUSED") || codes.includes("ConnectionRefused"))
|
|
306
|
+
return { kind: "not-running" };
|
|
307
|
+
}
|
|
308
|
+
return { kind: "unavailable" };
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
export async function handshake(record) {
|
|
312
|
+
return (await readLiveServiceStatus(record)).kind === "running";
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Clear the lifetime lock a killed owner left behind. The lease's process is
|
|
316
|
+
* gone, so acquiring the owner lock removes the marked directory;
|
|
317
|
+
* releasing it at once leaves nothing behind. A live owner makes this throw.
|
|
318
|
+
*/
|
|
319
|
+
export async function clearAbandonedServiceOwner(directory) {
|
|
320
|
+
const release = await acquireServiceLock(directory, "owner");
|
|
321
|
+
await release();
|
|
322
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
declare const ownerSchema: z.ZodObject<{
|
|
3
|
+
pid: z.ZodNumber;
|
|
4
|
+
startedAt: z.ZodString;
|
|
5
|
+
}, z.core.$strip>;
|
|
6
|
+
export type ServiceLeaseOwner = z.infer<typeof ownerSchema>;
|
|
7
|
+
/** The live claim's identity comes from SQLite, never from service.json. */
|
|
8
|
+
export declare function readServiceLeaseOwner(directory: string): Promise<ServiceLeaseOwner | undefined>;
|
|
9
|
+
/**
|
|
10
|
+
* Acquire the one process-owned row and publish its identity in the same
|
|
11
|
+
* transaction. This is a process-lifetime claim, not a heartbeat/timeout lease:
|
|
12
|
+
* only release or proof that the recorded process is gone permits replacement.
|
|
13
|
+
* Committing makes the identity readable by clients while the owner runs.
|
|
14
|
+
*/
|
|
15
|
+
export declare function acquireServiceOwnerLease(directory: string): Promise<() => void>;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { openPrivateWritableSqliteDatabase, } from "./private-sqlite-owner.js";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { readOwnProcessStart, readProcessStart, } from "./private-local-process-identity.js";
|
|
5
|
+
const ownerSchema = z.object({
|
|
6
|
+
pid: z.number().int().positive(),
|
|
7
|
+
startedAt: z.string().min(1),
|
|
8
|
+
});
|
|
9
|
+
function openLease(directory) {
|
|
10
|
+
const database = openPrivateWritableSqliteDatabase(join(directory, "owner.sqlite"), 0);
|
|
11
|
+
try {
|
|
12
|
+
database.exec("CREATE TABLE IF NOT EXISTS service_owner (id INTEGER PRIMARY KEY CHECK (id = 1), pid INTEGER NOT NULL, startedAt TEXT NOT NULL)");
|
|
13
|
+
return database;
|
|
14
|
+
}
|
|
15
|
+
catch (cause) {
|
|
16
|
+
database.close();
|
|
17
|
+
throw cause;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function readOwner(database) {
|
|
21
|
+
const row = database
|
|
22
|
+
.prepare("SELECT pid, startedAt FROM service_owner WHERE id = 1")
|
|
23
|
+
.get();
|
|
24
|
+
return row === undefined ? undefined : ownerSchema.parse(row);
|
|
25
|
+
}
|
|
26
|
+
async function ownerIsLive(owner) {
|
|
27
|
+
const current = await readProcessStart(owner.pid);
|
|
28
|
+
if (current.kind !== "unverifiable")
|
|
29
|
+
return current.kind === "running" && current.start === owner.startedAt;
|
|
30
|
+
// Without precise identity we may retain a stale claim, but never steal a
|
|
31
|
+
// possibly live one. Signal 0 checks existence; it does not terminate it.
|
|
32
|
+
try {
|
|
33
|
+
process.kill(owner.pid, 0);
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
catch (cause) {
|
|
37
|
+
if (cause instanceof Error && "code" in cause) {
|
|
38
|
+
if (cause.code === "ESRCH")
|
|
39
|
+
return false;
|
|
40
|
+
if (cause.code === "EPERM")
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
throw cause;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** The live claim's identity comes from SQLite, never from service.json. */
|
|
47
|
+
export async function readServiceLeaseOwner(directory) {
|
|
48
|
+
const database = openLease(directory);
|
|
49
|
+
try {
|
|
50
|
+
const owner = readOwner(database);
|
|
51
|
+
return owner !== undefined && (await ownerIsLive(owner))
|
|
52
|
+
? owner
|
|
53
|
+
: undefined;
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
database.close();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Acquire the one process-owned row and publish its identity in the same
|
|
61
|
+
* transaction. This is a process-lifetime claim, not a heartbeat/timeout lease:
|
|
62
|
+
* only release or proof that the recorded process is gone permits replacement.
|
|
63
|
+
* Committing makes the identity readable by clients while the owner runs.
|
|
64
|
+
*/
|
|
65
|
+
export async function acquireServiceOwnerLease(directory) {
|
|
66
|
+
const own = { pid: process.pid, startedAt: await readOwnProcessStart() };
|
|
67
|
+
const database = openLease(directory);
|
|
68
|
+
let transaction = false;
|
|
69
|
+
try {
|
|
70
|
+
database.exec("BEGIN IMMEDIATE");
|
|
71
|
+
transaction = true;
|
|
72
|
+
const owner = readOwner(database);
|
|
73
|
+
if (owner !== undefined && (await ownerIsLive(owner)))
|
|
74
|
+
throw new Error("The Fungi service owner database is locked by a live process");
|
|
75
|
+
database
|
|
76
|
+
.prepare("INSERT OR REPLACE INTO service_owner (id, pid, startedAt) VALUES (1, ?, ?)")
|
|
77
|
+
.run(own.pid, own.startedAt);
|
|
78
|
+
database.exec("COMMIT");
|
|
79
|
+
transaction = false;
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
try {
|
|
83
|
+
if (transaction)
|
|
84
|
+
database.exec("ROLLBACK");
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
database.close();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
let released = false;
|
|
91
|
+
return () => {
|
|
92
|
+
if (released)
|
|
93
|
+
return;
|
|
94
|
+
const database = openLease(directory);
|
|
95
|
+
try {
|
|
96
|
+
database
|
|
97
|
+
.prepare("DELETE FROM service_owner WHERE id = 1 AND pid = ? AND startedAt = ?")
|
|
98
|
+
.run(own.pid, own.startedAt);
|
|
99
|
+
released = true;
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
database.close();
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|