@fungi.computer/truffle 0.1.1 → 0.2.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.
Files changed (174) hide show
  1. package/README.md +282 -34
  2. package/dist/auth-cli.d.ts +10 -0
  3. package/dist/auth-cli.js +36 -14
  4. package/dist/fungi-account-command.d.ts +1 -1
  5. package/dist/fungi-account-command.js +6 -3
  6. package/dist/index.d.ts +10 -2
  7. package/dist/index.js +8 -2
  8. package/dist/internal/image-fit-metafile.json +106 -0
  9. package/dist/internal/local-image-fit-worker.mjs +960 -0
  10. package/dist/internal/private-workerd-sandbox-worker.mjs +11 -12
  11. package/dist/internal/sandbox-metafile.json +7 -7
  12. package/dist/local-archive-handoff.d.ts +12 -0
  13. package/dist/local-archive-handoff.js +21 -0
  14. package/dist/local-extensions.d.ts +58 -12
  15. package/dist/local-extensions.js +195 -55
  16. package/dist/local-image-fit.d.ts +30 -0
  17. package/dist/local-image-fit.js +105 -0
  18. package/dist/local-models.d.ts +4 -2
  19. package/dist/local-models.js +11 -6
  20. package/dist/local-session-archives.d.ts +8 -0
  21. package/dist/local-session-archives.js +152 -0
  22. package/dist/local-skill-source.d.ts +18 -3
  23. package/dist/local-skill-source.js +216 -140
  24. package/dist/log-event.d.ts +7 -0
  25. package/dist/log-event.js +8 -4
  26. package/dist/private-bounded-response.d.ts +2 -0
  27. package/dist/private-bounded-response.js +41 -0
  28. package/dist/private-file-credential-store.d.ts +28 -3
  29. package/dist/private-file-credential-store.js +57 -9
  30. package/dist/private-files-patch.d.ts +2 -1
  31. package/dist/private-files-patch.js +21 -3
  32. package/dist/private-fungi-account.d.ts +40 -23
  33. package/dist/private-fungi-account.js +201 -21
  34. package/dist/private-fungi-mcp-connection.d.ts +4 -0
  35. package/dist/private-fungi-mcp-connection.js +25 -6
  36. package/dist/private-fungi-mcp-selection-schema.d.ts +7 -0
  37. package/dist/private-fungi-mcp-selection-schema.js +15 -0
  38. package/dist/private-fungi-mcp-selection.d.ts +6 -6
  39. package/dist/private-fungi-mcp-selection.js +29 -16
  40. package/dist/private-fungi-model-api.js +4 -4
  41. package/dist/private-fungi-model-catalog.d.ts +10 -0
  42. package/dist/private-fungi-model-catalog.js +10 -2
  43. package/dist/private-fungi-model-provider.d.ts +1 -1
  44. package/dist/private-fungi-model-provider.js +6 -1
  45. package/dist/private-fungi-pending-logouts.d.ts +1 -1
  46. package/dist/private-fungi-pending-logouts.js +1 -2
  47. package/dist/private-fungi-service-logout.d.ts +44 -0
  48. package/dist/private-fungi-service-logout.js +59 -0
  49. package/dist/private-invocation-computer.d.ts +24 -3
  50. package/dist/private-invocation-computer.js +101 -62
  51. package/dist/private-invocation-files.d.ts +16 -1
  52. package/dist/private-invocation-files.js +154 -83
  53. package/dist/private-local-acp-stdio.js +3 -0
  54. package/dist/private-local-admission-gate.d.ts +14 -0
  55. package/dist/private-local-admission-gate.js +52 -0
  56. package/dist/private-local-agent-control.d.ts +101 -0
  57. package/dist/private-local-agent-control.js +30 -0
  58. package/dist/private-local-agent-default-model.d.ts +12 -0
  59. package/dist/private-local-agent-default-model.js +31 -0
  60. package/dist/private-local-auth-rpc.d.ts +67 -7
  61. package/dist/private-local-auth-rpc.js +140 -17
  62. package/dist/private-local-bwrap.d.ts +13 -0
  63. package/dist/private-local-bwrap.js +70 -0
  64. package/dist/private-local-client-version.d.ts +5 -0
  65. package/dist/private-local-client-version.js +43 -0
  66. package/dist/private-local-computer-attach.d.ts +3 -3
  67. package/dist/private-local-computer-attach.js +17 -3
  68. package/dist/private-local-computer-processes.d.ts +6 -3
  69. package/dist/private-local-computer-processes.js +55 -31
  70. package/dist/private-local-config-root.d.ts +21 -0
  71. package/dist/private-local-config-root.js +19 -0
  72. package/dist/private-local-containment.d.ts +51 -0
  73. package/dist/private-local-containment.js +161 -0
  74. package/dist/private-local-delegation.d.ts +10 -3
  75. package/dist/private-local-delegation.js +70 -15
  76. package/dist/private-local-effective-model.d.ts +9 -1
  77. package/dist/private-local-effective-model.js +3 -0
  78. package/dist/private-local-exec-environment.d.ts +16 -0
  79. package/dist/private-local-exec-environment.js +11 -0
  80. package/dist/private-local-linux-sandbox.d.ts +8 -0
  81. package/dist/private-local-linux-sandbox.js +67 -0
  82. package/dist/private-local-mail.d.ts +4 -2
  83. package/dist/private-local-mail.js +41 -33
  84. package/dist/private-local-mesh-enrollment.d.ts +34 -0
  85. package/dist/private-local-mesh-enrollment.js +188 -0
  86. package/dist/private-local-mesh-links.d.ts +21 -0
  87. package/dist/private-local-mesh-links.js +229 -0
  88. package/dist/private-local-mesh-record.d.ts +120 -0
  89. package/dist/private-local-mesh-record.js +55 -0
  90. package/dist/private-local-process-identity.d.ts +20 -0
  91. package/dist/private-local-process-identity.js +40 -0
  92. package/dist/private-local-runtime-host.d.ts +64 -13
  93. package/dist/private-local-runtime-host.js +361 -172
  94. package/dist/private-local-sandbox-policy.d.ts +21 -0
  95. package/dist/private-local-sandbox-policy.js +121 -0
  96. package/dist/private-local-sandbox.d.ts +37 -0
  97. package/dist/private-local-sandbox.js +114 -0
  98. package/dist/private-local-seatbelt.d.ts +24 -0
  99. package/dist/private-local-seatbelt.js +68 -0
  100. package/dist/private-local-service-contract.d.ts +91 -11
  101. package/dist/private-local-service-discovery.d.ts +129 -13
  102. package/dist/private-local-service-discovery.js +148 -47
  103. package/dist/private-local-service-lease.d.ts +16 -0
  104. package/dist/private-local-service-lease.js +105 -0
  105. package/dist/private-local-service-listener.d.ts +9 -2
  106. package/dist/private-local-service-listener.js +260 -64
  107. package/dist/private-local-service-log.d.ts +128 -0
  108. package/dist/private-local-service-log.js +321 -0
  109. package/dist/private-local-service-owner.d.ts +87 -1519
  110. package/dist/private-local-service-owner.js +1069 -71
  111. package/dist/private-local-service-process.js +3 -1
  112. package/dist/private-local-service-termination.d.ts +7 -0
  113. package/dist/private-local-service-termination.js +67 -0
  114. package/dist/private-local-service.d.ts +31 -2
  115. package/dist/private-local-service.js +402 -126
  116. package/dist/private-local-session-discovery.d.ts +52 -5
  117. package/dist/private-local-session-discovery.js +132 -11
  118. package/dist/private-local-shiitake-host.js +2 -2
  119. package/dist/private-local-startup-model.d.ts +51 -0
  120. package/dist/private-local-startup-model.js +86 -0
  121. package/dist/private-local-stop-all.d.ts +27 -0
  122. package/dist/private-local-stop-all.js +26 -0
  123. package/dist/private-local-ui-attachment.d.ts +19 -0
  124. package/dist/private-local-ui-attachment.js +71 -0
  125. package/dist/private-local-worker-server.d.ts +1 -0
  126. package/dist/private-local-worker-server.js +13 -5
  127. package/dist/private-mcp-form-contract.d.ts +186 -0
  128. package/dist/private-mcp-form-contract.js +53 -0
  129. package/dist/private-mcp-form-elicitation.d.ts +18 -0
  130. package/dist/private-mcp-form-elicitation.js +97 -0
  131. package/dist/private-mcp-form-session.d.ts +172 -0
  132. package/dist/private-mcp-form-session.js +148 -0
  133. package/dist/private-openai-claims.d.ts +30 -0
  134. package/dist/private-openai-claims.js +69 -0
  135. package/dist/private-oyster-mcp-auth.d.ts +13 -0
  136. package/dist/private-oyster-mcp-auth.js +57 -0
  137. package/dist/private-provider-rejection.d.ts +15 -0
  138. package/dist/private-provider-rejection.js +24 -0
  139. package/dist/private-sqlite-owner.d.ts +34 -2
  140. package/dist/private-sqlite-owner.js +103 -37
  141. package/dist/private-team-computers.d.ts +4 -4
  142. package/dist/private-team-computers.js +11 -2
  143. package/dist/seatbelt/safe-base.sbpl +79 -0
  144. package/dist/subscription-activation.d.ts +9 -0
  145. package/dist/subscription-activation.js +62 -0
  146. package/dist/subscription-identity.d.ts +23 -0
  147. package/dist/subscription-identity.js +124 -0
  148. package/dist/subscription-turn.d.ts +17 -0
  149. package/dist/subscription-turn.js +143 -0
  150. package/dist/team-apps.d.ts +2 -2
  151. package/dist/team-apps.js +7 -2
  152. package/native/bin/LICENSE.bubblewrap +502 -0
  153. package/native/bin/LICENSE.libcap +112 -0
  154. package/native/bin/LICENSE.sandbox-runtime +201 -0
  155. package/native/bin/SOURCE.bubblewrap.tar.xz +0 -0
  156. package/native/bin/bwrap-build-source/Dockerfile +48 -0
  157. package/native/bin/bwrap-build-source/README.md +96 -0
  158. package/native/bin/bwrap-build-source/build.mjs +52 -0
  159. package/native/bin/bwrap-build-source/linux-arm64.ini +14 -0
  160. package/native/bin/bwrap-build-source/pin.mjs +21 -0
  161. package/native/bin/bwrap-build-source/record.mjs +74 -0
  162. package/native/bin/bwrap-build-source/vendor/LICENSE.sandbox-runtime +201 -0
  163. package/native/bin/bwrap-build-source/vendor/README.md +23 -0
  164. package/native/bin/bwrap-build-source/vendor/seccomp-unix-block.c +280 -0
  165. package/native/bin/bwrap-build.json +44 -0
  166. package/native/bin/fungi-bwrap +0 -0
  167. package/native/bin/fungi-bwrap.sha256 +1 -0
  168. package/native/bin/fungi-seccomp.bpf +0 -0
  169. package/native/bin/fungi-seccomp.bpf.sha256 +1 -0
  170. package/package.json +44 -31
  171. package/dist/private-local-model-settings.d.ts +0 -5
  172. package/dist/private-local-model-settings.js +0 -31
  173. package/dist/private-local-subagents.d.ts +0 -27
  174. package/dist/private-local-subagents.js +0 -77
@@ -1,38 +1,112 @@
1
1
  import { z } from "zod";
2
- declare const serviceRecordSchema: z.ZodObject<{
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;
3
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>;
4
23
  baseUrl: z.ZodURL;
5
- }, z.core.$strict>;
6
- declare const serviceStatusSchema: z.ZodObject<{
24
+ version: z.ZodString;
7
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>>;
8
- baseUrl: z.ZodURL;
9
- buildId: z.ZodOptional<z.ZodString>;
26
+ pid: z.ZodNumber;
27
+ startedAt: z.ZodString;
10
28
  }, z.core.$strict>;
11
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
+ }>;
12
72
  export declare const selectionSchema: z.ZodObject<{
73
+ uiAttachmentLifetime: z.ZodOptional<z.ZodEnum<{
74
+ watch: "watch";
75
+ connection: "connection";
76
+ }>>;
13
77
  invocationRoot: z.ZodString;
14
78
  clientPath: z.ZodString;
79
+ requireSandbox: z.ZodOptional<z.ZodBoolean>;
15
80
  databasePath: z.ZodOptional<z.ZodString>;
16
81
  sessionId: z.ZodOptional<z.ZodString>;
17
82
  fungiMcp: z.ZodOptional<z.ZodObject<{
18
83
  endpoint: z.ZodPipe<z.ZodURL, z.ZodString>;
84
+ authentication: z.ZodOptional<z.ZodLiteral<"oyster-key">>;
19
85
  }, z.core.$strict>>;
20
86
  }, z.core.$strict>;
21
87
  export declare const sessionCreationSchema: z.ZodObject<{
88
+ uiAttachmentLifetime: z.ZodOptional<z.ZodEnum<{
89
+ watch: "watch";
90
+ connection: "connection";
91
+ }>>;
22
92
  invocationRoot: z.ZodString;
23
93
  clientPath: z.ZodString;
94
+ requireSandbox: z.ZodOptional<z.ZodBoolean>;
24
95
  databasePath: z.ZodString;
25
96
  requestId: z.ZodString;
26
97
  fungiMcp: z.ZodOptional<z.ZodObject<{
27
98
  endpoint: z.ZodPipe<z.ZodURL, z.ZodString>;
99
+ authentication: z.ZodOptional<z.ZodLiteral<"oyster-key">>;
28
100
  }, z.core.$strict>>;
29
101
  }, z.core.$strict>;
30
102
  export declare const sessionInspectionSchema: z.ZodObject<{
103
+ databasePath: z.ZodOptional<z.ZodString>;
104
+ invocationRoot: z.ZodString;
31
105
  fungiMcp: z.ZodOptional<z.ZodObject<{
32
106
  endpoint: z.ZodPipe<z.ZodURL, z.ZodString>;
107
+ authentication: z.ZodOptional<z.ZodLiteral<"oyster-key">>;
33
108
  }, z.core.$strict>>;
34
- invocationRoot: z.ZodString;
35
- databasePath: z.ZodOptional<z.ZodString>;
109
+ requireSandbox: z.ZodOptional<z.ZodBoolean>;
36
110
  sessionId: z.ZodString;
37
111
  limit: z.ZodNumber;
38
112
  }, z.core.$strict>;
@@ -40,11 +114,41 @@ export declare const selectedSchema: z.ZodObject<{
40
114
  agentId: z.ZodString;
41
115
  databasePath: z.ZodString;
42
116
  sessionId: z.ZodString;
43
- routePath: 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;
44
140
  }, z.core.$strict>;
45
141
  export declare const sessionDiscoverySchema: z.ZodObject<{
46
142
  root: z.ZodString;
47
- agentCount: z.ZodNumber;
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>>;
48
152
  sessions: z.ZodArray<z.ZodObject<{
49
153
  databasePath: z.ZodString;
50
154
  sessionId: z.ZodString;
@@ -55,8 +159,8 @@ export declare const sessionDiscoverySchema: z.ZodObject<{
55
159
  problems: z.ZodArray<z.ZodObject<{
56
160
  kind: z.ZodEnum<{
57
161
  corrupt: "corrupt";
58
- in_use: "in_use";
59
162
  unreadable: "unreadable";
163
+ in_use: "in_use";
60
164
  }>;
61
165
  }, z.core.$strict>>;
62
166
  }, z.core.$strict>;
@@ -74,11 +178,18 @@ export declare const modelChoicesSchema: z.ZodArray<z.ZodObject<{
74
178
  max: "max";
75
179
  }>>;
76
180
  }, z.core.$strict>>;
181
+ /** Resolve a read target without creating/chmodding a directory. */
182
+ export declare function serviceDirectoryPath(directory?: string): string;
77
183
  export declare function serviceDirectory(directory?: string): Promise<string>;
78
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. */
79
188
  export declare function readServiceRecord(directory: string): Promise<ServiceRecord | undefined>;
80
189
  /** An older listener must not erase its successor's discovery record. */
81
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>;
82
193
  /** Legacy owners have no SQLite lease, so their file lock must not be stolen. */
83
194
  export declare function assertNoUntrackedServiceOwner(directory: string): Promise<void>;
84
195
  /** Startup and shutdown serialize with the native listener and lifetime owner lock. */
@@ -87,10 +198,10 @@ export declare function acquireServiceLock(directory: string, name: "startup" |
87
198
  * Whether a service process holds this directory's lifetime lease. A held
88
199
  * lease is a live owner even when it is too busy to answer its handshake.
89
200
  */
90
- export declare function serviceOwnerIsLive(directory: string): boolean;
201
+ export declare function serviceOwnerIsLive(directory: string): Promise<boolean>;
91
202
  export type ServiceProbe = Readonly<{
92
203
  kind: "running";
93
- service: z.infer<typeof serviceStatusSchema>;
204
+ service: LiveServiceStatus;
94
205
  }> | Readonly<{
95
206
  kind: "not-running";
96
207
  }> | Readonly<{
@@ -98,4 +209,9 @@ export type ServiceProbe = Readonly<{
98
209
  }>;
99
210
  export declare function readLiveServiceStatus(record: ServiceRecord, timeoutMs?: number): Promise<ServiceProbe>;
100
211
  export declare function handshake(record: ServiceRecord): Promise<boolean>;
101
- export {};
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>;
@@ -1,4 +1,5 @@
1
- import { fungiMcpSelectionSchema } from "./private-fungi-mcp-selection.js";
1
+ import { fungiMcpSelectionSchema } from "./private-fungi-mcp-selection-schema.js";
2
+ import { localUiAttachmentLifetimeSchema } from "./private-local-ui-attachment.js";
2
3
  import { privateLocalExecPathSchema } from "./private-local-exec-path.js";
3
4
  import { chmod, mkdir, readFile, realpath, rm, rmdir, stat, writeFile, } from "node:fs/promises";
4
5
  import { join, resolve } from "node:path";
@@ -6,10 +7,43 @@ import { homedir } from "node:os";
6
7
  import lockfile from "proper-lockfile";
7
8
  import { z } from "zod";
8
9
  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({
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),
12
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),
13
47
  baseUrl: z.url().refine((value) => {
14
48
  const url = new URL(value);
15
49
  return (url.protocol === "http:" &&
@@ -22,28 +56,30 @@ const serviceRecordSchema = z.strictObject({
22
56
  url.hash === "");
23
57
  }),
24
58
  });
25
- const serviceStatusSchema = serviceRecordSchema.safeExtend({
26
- buildId: z
27
- .string()
28
- .regex(/^[a-f0-9]{64}$/u)
29
- .optional(),
59
+ /** Mandatory live diagnostics; never written into the registration pointer. */
60
+ export const liveServiceStatusSchema = serviceRecordSchema.extend({
61
+ diagnostics: serviceDiagnosticsStatusSchema,
30
62
  });
31
63
  export const selectionSchema = z.strictObject({
64
+ uiAttachmentLifetime: localUiAttachmentLifetimeSchema.optional(),
32
65
  invocationRoot: z.string().min(1),
33
66
  clientPath: privateLocalExecPathSchema,
67
+ requireSandbox: z.boolean().optional(),
34
68
  databasePath: z.string().min(1).optional(),
35
69
  sessionId: z.string().min(1).optional(),
36
70
  fungiMcp: fungiMcpSelectionSchema.optional(),
37
71
  });
38
72
  export const sessionCreationSchema = z.strictObject({
73
+ uiAttachmentLifetime: localUiAttachmentLifetimeSchema.optional(),
39
74
  invocationRoot: z.string().min(1),
40
75
  clientPath: privateLocalExecPathSchema,
76
+ requireSandbox: z.boolean().optional(),
41
77
  databasePath: z.string().min(1),
42
78
  requestId: z.string().min(1),
43
79
  fungiMcp: fungiMcpSelectionSchema.optional(),
44
80
  });
45
81
  export const sessionInspectionSchema = selectionSchema
46
- .omit({ clientPath: true })
82
+ .omit({ clientPath: true, uiAttachmentLifetime: true })
47
83
  .extend({
48
84
  sessionId: z.string().min(1),
49
85
  limit: z.number().int().min(1).max(200),
@@ -52,11 +88,34 @@ export const selectedSchema = z.strictObject({
52
88
  agentId: z.string().min(1),
53
89
  databasePath: z.string().min(1),
54
90
  sessionId: z.string().min(1),
55
- routePath: z.string().regex(/^\/agents\/[0-9]+$/),
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(),
56
107
  });
57
108
  export const sessionDiscoverySchema = z.strictObject({
58
109
  root: z.string().min(1),
59
- agentCount: z.number().int().nonnegative(),
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
+ })),
60
119
  sessions: z.array(z.strictObject({
61
120
  databasePath: z.string().min(1),
62
121
  sessionId: z.string().min(1),
@@ -66,41 +125,84 @@ export const sessionDiscoverySchema = z.strictObject({
66
125
  })),
67
126
  problems: z.array(z.strictObject({ kind: z.enum(["corrupt", "in_use", "unreadable"]) })),
68
127
  });
69
- const reasoningLevels = [
70
- "minimal",
71
- "low",
72
- "medium",
73
- "high",
74
- "xhigh",
75
- "max",
76
- ];
77
128
  export const modelChoicesSchema = z.array(z.strictObject({
78
129
  providerId: z.string().min(1),
79
130
  providerName: z.string().min(1),
80
131
  id: z.string().min(1),
81
132
  name: z.string().min(1),
82
- reasoning: z.array(z.enum(reasoningLevels)),
133
+ reasoning: z.array(z.enum(modelReasoningLevels)),
83
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
+ }
84
139
  export async function serviceDirectory(directory) {
85
- const path = resolve(directory ?? join(homedir(), ".fungi", "service"));
140
+ const path = serviceDirectoryPath(directory);
86
141
  await mkdir(path, { recursive: true, mode: 0o700 });
87
142
  await chmod(path, 0o700);
88
143
  return realpath(path);
89
144
  }
90
145
  export const recordPath = (directory) => join(directory, "service.json");
91
- export async function readServiceRecord(directory) {
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;
92
149
  try {
93
- return serviceRecordSchema.parse(JSON.parse(await readFile(recordPath(directory), "utf8")));
150
+ text = await readFile(recordPath(directory), "utf8");
94
151
  }
95
152
  catch (cause) {
96
153
  if (cause instanceof Error && "code" in cause && cause.code === "ENOENT")
97
154
  return undefined;
98
155
  throw cause;
99
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;
100
193
  }
101
194
  /** An older listener must not erase its successor's discovery record. */
102
195
  export async function removeOwnedServiceRecord(directory, serviceId) {
103
- if ((await readServiceRecord(directory))?.serviceId === 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")
104
206
  await rm(recordPath(directory), { force: true });
105
207
  }
106
208
  const ownerLeaseMarker = (directory) => join(directory, "owner.lock", "sqlite-owner");
@@ -132,16 +234,13 @@ export async function acquireServiceLock(directory, name) {
132
234
  throw error;
133
235
  },
134
236
  });
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"));
237
+ const releaseLease = await acquireServiceOwnerLease(directory);
139
238
  const lockPath = join(directory, "owner.lock");
140
239
  let created = false;
141
240
  try {
142
241
  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.
242
+ // A dead owner can leave a marked directory behind. Its process claim has
243
+ // already been replaced transactionally, without trusting a heartbeat.
145
244
  if (await pathExists(ownerLeaseMarker(directory)))
146
245
  await rm(lockPath, { recursive: true });
147
246
  await mkdir(lockPath);
@@ -157,7 +256,7 @@ export async function acquireServiceLock(directory, name) {
157
256
  await rm(lockPath, { recursive: true });
158
257
  }
159
258
  finally {
160
- lease.release();
259
+ releaseLease();
161
260
  }
162
261
  throw cause;
163
262
  }
@@ -167,7 +266,7 @@ export async function acquireServiceLock(directory, name) {
167
266
  await rmdir(lockPath);
168
267
  }
169
268
  finally {
170
- lease.release();
269
+ releaseLease();
171
270
  }
172
271
  };
173
272
  }
@@ -175,18 +274,8 @@ export async function acquireServiceLock(directory, name) {
175
274
  * Whether a service process holds this directory's lifetime lease. A held
176
275
  * lease is a live owner even when it is too busy to answer its handshake.
177
276
  */
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;
277
+ export async function serviceOwnerIsLive(directory) {
278
+ return (await readServiceLeaseOwner(directory)) !== undefined;
190
279
  }
191
280
  const transportFailureSchema = z.looseObject({
192
281
  code: z.string().optional(),
@@ -200,9 +289,12 @@ export async function readLiveServiceStatus(record, timeoutMs = 2000) {
200
289
  });
201
290
  if (!response.ok)
202
291
  return { kind: "unavailable" };
203
- const actual = serviceStatusSchema.parse(await response.json());
292
+ const actual = liveServiceStatusSchema.parse(await response.json());
204
293
  return actual.serviceId === record.serviceId &&
205
- actual.baseUrl === record.baseUrl
294
+ actual.baseUrl === record.baseUrl &&
295
+ actual.pid === record.pid &&
296
+ actual.startedAt === record.startedAt &&
297
+ actual.version === record.version
206
298
  ? { kind: "running", service: actual }
207
299
  : { kind: "unavailable" };
208
300
  }
@@ -219,3 +311,12 @@ export async function readLiveServiceStatus(record, timeoutMs = 2000) {
219
311
  export async function handshake(record) {
220
312
  return (await readLiveServiceStatus(record)).kind === "running";
221
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
+ }
@@ -1,15 +1,22 @@
1
+ import { type ServiceDrainPhase } from "./private-local-service-log.js";
1
2
  import { runLocalAuthCli } from "./auth-cli.js";
3
+ import { type PrivateLocalConfigRoot } from "./private-local-config-root.js";
2
4
  import { listPrivateModels } from "./local-models.js";
3
5
  import { type OpenLocalServiceRuntime } from "./private-local-service-owner.js";
4
6
  export declare function createPrivateLocalServiceShutdown(input: {
5
7
  closeOwners: () => Promise<void>;
6
8
  closeServer: () => Promise<void>;
9
+ closeDiagnostics?: () => void;
7
10
  removeRecord: () => Promise<void>;
8
11
  releaseOwner: () => Promise<void>;
9
12
  }): {
10
- drain: () => Promise<void>;
11
13
  finish: () => Promise<void>;
12
14
  isDraining: () => boolean;
15
+ phase: () => ServiceDrainPhase;
13
16
  };
14
17
  /** 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>;
18
+ export declare function runPrivateLocalService(directory: string, openRuntime?: OpenLocalServiceRuntime, listModels?: typeof listPrivateModels, runAuth?: typeof runLocalAuthCli,
19
+ /** How often the service releases Agents nothing uses. */
20
+ idleSweepMs?: number,
21
+ /** Narrow manifest I/O port, independent of the Agent-runtime opener. */
22
+ readRecoveryManifest?: (path: string) => Promise<string>, root?: PrivateLocalConfigRoot): Promise<void>;