@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.
Files changed (207) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +542 -2
  3. package/computerd-loopback.cjs +16 -0
  4. package/dist/auth-cli.d.ts +46 -0
  5. package/dist/auth-cli.js +373 -0
  6. package/dist/fungi-account-command.d.ts +18 -0
  7. package/dist/fungi-account-command.js +95 -0
  8. package/dist/index.d.ts +27 -0
  9. package/dist/index.js +23 -0
  10. package/dist/internal/image-fit-metafile.json +106 -0
  11. package/dist/internal/local-image-fit-worker.mjs +960 -0
  12. package/dist/internal/private-workerd-sandbox-worker.mjs +27487 -0
  13. package/dist/internal/sandbox-metafile.json +6726 -0
  14. package/dist/linear-auth.d.ts +4 -0
  15. package/dist/linear-auth.js +72 -0
  16. package/dist/local-archive-handoff.d.ts +12 -0
  17. package/dist/local-archive-handoff.js +21 -0
  18. package/dist/local-extensions.d.ts +143 -0
  19. package/dist/local-extensions.js +624 -0
  20. package/dist/local-image-fit.d.ts +30 -0
  21. package/dist/local-image-fit.js +105 -0
  22. package/dist/local-login.d.ts +55 -0
  23. package/dist/local-login.js +167 -0
  24. package/dist/local-models.d.ts +16 -0
  25. package/dist/local-models.js +82 -0
  26. package/dist/local-session-archives.d.ts +8 -0
  27. package/dist/local-session-archives.js +152 -0
  28. package/dist/local-skill-source.d.ts +21 -0
  29. package/dist/local-skill-source.js +433 -0
  30. package/dist/log-event.d.ts +9 -0
  31. package/dist/log-event.js +13 -0
  32. package/dist/private-bounded-response.d.ts +2 -0
  33. package/dist/private-bounded-response.js +41 -0
  34. package/dist/private-bun-shiitake-host.d.ts +11 -0
  35. package/dist/private-bun-shiitake-host.js +66 -0
  36. package/dist/private-file-credential-store.d.ts +58 -0
  37. package/dist/private-file-credential-store.js +331 -0
  38. package/dist/private-files-patch.d.ts +4 -0
  39. package/dist/private-files-patch.js +104 -0
  40. package/dist/private-fungi-account.d.ts +252 -0
  41. package/dist/private-fungi-account.js +1329 -0
  42. package/dist/private-fungi-auth-error.d.ts +5 -0
  43. package/dist/private-fungi-auth-error.js +7 -0
  44. package/dist/private-fungi-mcp-connection.d.ts +26 -0
  45. package/dist/private-fungi-mcp-connection.js +162 -0
  46. package/dist/private-fungi-mcp-endpoint.d.ts +2 -0
  47. package/dist/private-fungi-mcp-endpoint.js +15 -0
  48. package/dist/private-fungi-mcp-selection-schema.d.ts +7 -0
  49. package/dist/private-fungi-mcp-selection-schema.js +15 -0
  50. package/dist/private-fungi-mcp-selection.d.ts +14 -0
  51. package/dist/private-fungi-mcp-selection.js +49 -0
  52. package/dist/private-fungi-model-api.d.ts +6 -0
  53. package/dist/private-fungi-model-api.js +125 -0
  54. package/dist/private-fungi-model-catalog.d.ts +16 -0
  55. package/dist/private-fungi-model-catalog.js +86 -0
  56. package/dist/private-fungi-model-provider.d.ts +6 -0
  57. package/dist/private-fungi-model-provider.js +51 -0
  58. package/dist/private-fungi-pending-logouts.d.ts +34 -0
  59. package/dist/private-fungi-pending-logouts.js +135 -0
  60. package/dist/private-fungi-service-logout.d.ts +44 -0
  61. package/dist/private-fungi-service-logout.js +59 -0
  62. package/dist/private-invocation-computer.d.ts +58 -0
  63. package/dist/private-invocation-computer.js +498 -0
  64. package/dist/private-invocation-files.d.ts +30 -0
  65. package/dist/private-invocation-files.js +520 -0
  66. package/dist/private-local-acp-stdio.d.ts +17 -0
  67. package/dist/private-local-acp-stdio.js +145 -0
  68. package/dist/private-local-admission-gate.d.ts +14 -0
  69. package/dist/private-local-admission-gate.js +52 -0
  70. package/dist/private-local-agent-control.d.ts +101 -0
  71. package/dist/private-local-agent-control.js +30 -0
  72. package/dist/private-local-agent-default-model.d.ts +12 -0
  73. package/dist/private-local-agent-default-model.js +31 -0
  74. package/dist/private-local-auth-rpc.d.ts +129 -0
  75. package/dist/private-local-auth-rpc.js +396 -0
  76. package/dist/private-local-bwrap.d.ts +13 -0
  77. package/dist/private-local-bwrap.js +70 -0
  78. package/dist/private-local-client-version.d.ts +5 -0
  79. package/dist/private-local-client-version.js +43 -0
  80. package/dist/private-local-computer-attach.d.ts +49 -0
  81. package/dist/private-local-computer-attach.js +230 -0
  82. package/dist/private-local-computer-backend.d.ts +282 -0
  83. package/dist/private-local-computer-backend.js +402 -0
  84. package/dist/private-local-computer-processes.d.ts +202 -0
  85. package/dist/private-local-computer-processes.js +613 -0
  86. package/dist/private-local-config-root.d.ts +21 -0
  87. package/dist/private-local-config-root.js +19 -0
  88. package/dist/private-local-containment.d.ts +51 -0
  89. package/dist/private-local-containment.js +161 -0
  90. package/dist/private-local-default-model.d.ts +11 -0
  91. package/dist/private-local-default-model.js +45 -0
  92. package/dist/private-local-delegation.d.ts +36 -0
  93. package/dist/private-local-delegation.js +133 -0
  94. package/dist/private-local-effective-model.d.ts +27 -0
  95. package/dist/private-local-effective-model.js +17 -0
  96. package/dist/private-local-exec-environment.d.ts +16 -0
  97. package/dist/private-local-exec-environment.js +11 -0
  98. package/dist/private-local-exec-launch.d.ts +41 -0
  99. package/dist/private-local-exec-launch.js +51 -0
  100. package/dist/private-local-exec-path.d.ts +6 -0
  101. package/dist/private-local-exec-path.js +40 -0
  102. package/dist/private-local-git-exclude.d.ts +2 -0
  103. package/dist/private-local-git-exclude.js +33 -0
  104. package/dist/private-local-http-dispatch.d.ts +5 -0
  105. package/dist/private-local-http-dispatch.js +14 -0
  106. package/dist/private-local-linux-sandbox.d.ts +8 -0
  107. package/dist/private-local-linux-sandbox.js +67 -0
  108. package/dist/private-local-mail.d.ts +13 -0
  109. package/dist/private-local-mail.js +77 -0
  110. package/dist/private-local-mesh-enrollment.d.ts +34 -0
  111. package/dist/private-local-mesh-enrollment.js +188 -0
  112. package/dist/private-local-mesh-links.d.ts +21 -0
  113. package/dist/private-local-mesh-links.js +229 -0
  114. package/dist/private-local-mesh-record.d.ts +120 -0
  115. package/dist/private-local-mesh-record.js +55 -0
  116. package/dist/private-local-process-identity.d.ts +20 -0
  117. package/dist/private-local-process-identity.js +40 -0
  118. package/dist/private-local-runtime-host.d.ts +192 -0
  119. package/dist/private-local-runtime-host.js +1177 -0
  120. package/dist/private-local-sandbox-policy.d.ts +21 -0
  121. package/dist/private-local-sandbox-policy.js +121 -0
  122. package/dist/private-local-sandbox.d.ts +37 -0
  123. package/dist/private-local-sandbox.js +114 -0
  124. package/dist/private-local-seatbelt.d.ts +24 -0
  125. package/dist/private-local-seatbelt.js +68 -0
  126. package/dist/private-local-service-contract.d.ts +170 -0
  127. package/dist/private-local-service-contract.js +1 -0
  128. package/dist/private-local-service-discovery.d.ts +217 -0
  129. package/dist/private-local-service-discovery.js +322 -0
  130. package/dist/private-local-service-ids.d.ts +3 -0
  131. package/dist/private-local-service-ids.js +8 -0
  132. package/dist/private-local-service-lease.d.ts +16 -0
  133. package/dist/private-local-service-lease.js +105 -0
  134. package/dist/private-local-service-listener.d.ts +22 -0
  135. package/dist/private-local-service-listener.js +407 -0
  136. package/dist/private-local-service-log.d.ts +128 -0
  137. package/dist/private-local-service-log.js +321 -0
  138. package/dist/private-local-service-owner.d.ts +135 -0
  139. package/dist/private-local-service-owner.js +1369 -0
  140. package/dist/private-local-service-process.d.ts +1 -0
  141. package/dist/private-local-service-process.js +8 -0
  142. package/dist/private-local-service-termination.d.ts +7 -0
  143. package/dist/private-local-service-termination.js +67 -0
  144. package/dist/private-local-service.d.ts +46 -0
  145. package/dist/private-local-service.js +606 -0
  146. package/dist/private-local-session-discovery.d.ts +66 -0
  147. package/dist/private-local-session-discovery.js +278 -0
  148. package/dist/private-local-shiitake-host.d.ts +44 -0
  149. package/dist/private-local-shiitake-host.js +143 -0
  150. package/dist/private-local-startup-model.d.ts +51 -0
  151. package/dist/private-local-startup-model.js +86 -0
  152. package/dist/private-local-stop-all.d.ts +27 -0
  153. package/dist/private-local-stop-all.js +26 -0
  154. package/dist/private-local-ui-attachment.d.ts +19 -0
  155. package/dist/private-local-ui-attachment.js +71 -0
  156. package/dist/private-local-worker-server.d.ts +16 -0
  157. package/dist/private-local-worker-server.js +144 -0
  158. package/dist/private-mcp-form-contract.d.ts +186 -0
  159. package/dist/private-mcp-form-contract.js +53 -0
  160. package/dist/private-mcp-form-elicitation.d.ts +18 -0
  161. package/dist/private-mcp-form-elicitation.js +97 -0
  162. package/dist/private-mcp-form-session.d.ts +172 -0
  163. package/dist/private-mcp-form-session.js +148 -0
  164. package/dist/private-openai-claims.d.ts +30 -0
  165. package/dist/private-openai-claims.js +69 -0
  166. package/dist/private-oyster-mcp-auth.d.ts +13 -0
  167. package/dist/private-oyster-mcp-auth.js +57 -0
  168. package/dist/private-provider-rejection.d.ts +15 -0
  169. package/dist/private-provider-rejection.js +24 -0
  170. package/dist/private-sqlite-owner.d.ts +39 -0
  171. package/dist/private-sqlite-owner.js +169 -0
  172. package/dist/private-team-computers.d.ts +38 -0
  173. package/dist/private-team-computers.js +110 -0
  174. package/dist/private-workerd-sandbox.d.ts +3 -0
  175. package/dist/private-workerd-sandbox.js +347 -0
  176. package/dist/seatbelt/safe-base.sbpl +79 -0
  177. package/dist/subscription-activation.d.ts +9 -0
  178. package/dist/subscription-activation.js +62 -0
  179. package/dist/subscription-identity.d.ts +23 -0
  180. package/dist/subscription-identity.js +124 -0
  181. package/dist/subscription-turn.d.ts +17 -0
  182. package/dist/subscription-turn.js +143 -0
  183. package/dist/team-apps.d.ts +24 -0
  184. package/dist/team-apps.js +128 -0
  185. package/dist/tracker-git-credential.d.ts +15 -0
  186. package/dist/tracker-git-credential.js +87 -0
  187. package/native/bin/LICENSE.bubblewrap +502 -0
  188. package/native/bin/LICENSE.libcap +112 -0
  189. package/native/bin/LICENSE.sandbox-runtime +201 -0
  190. package/native/bin/SOURCE.bubblewrap.tar.xz +0 -0
  191. package/native/bin/build.json +4037 -0
  192. package/native/bin/bwrap-build-source/Dockerfile +48 -0
  193. package/native/bin/bwrap-build-source/README.md +96 -0
  194. package/native/bin/bwrap-build-source/build.mjs +52 -0
  195. package/native/bin/bwrap-build-source/linux-arm64.ini +14 -0
  196. package/native/bin/bwrap-build-source/pin.mjs +21 -0
  197. package/native/bin/bwrap-build-source/record.mjs +74 -0
  198. package/native/bin/bwrap-build-source/vendor/LICENSE.sandbox-runtime +201 -0
  199. package/native/bin/bwrap-build-source/vendor/README.md +23 -0
  200. package/native/bin/bwrap-build-source/vendor/seccomp-unix-block.c +280 -0
  201. package/native/bin/bwrap-build.json +44 -0
  202. package/native/bin/fungi-apply-patch +0 -0
  203. package/native/bin/fungi-bwrap +0 -0
  204. package/native/bin/fungi-bwrap.sha256 +1 -0
  205. package/native/bin/fungi-seccomp.bpf +0 -0
  206. package/native/bin/fungi-seccomp.bpf.sha256 +1 -0
  207. package/package.json +161 -3
@@ -0,0 +1,520 @@
1
+ import { lstat, open, opendir, realpath, stat } from "node:fs/promises";
2
+ import { isAbsolute, join, matchesGlob, normalize, relative, resolve, sep, } from "node:path";
3
+ import { FilesEditError } from "@fungi.computer/knapsack";
4
+ import { SearchVisibility } from "@fungi.computer/knapsack/files-search";
5
+ import { imageReadResult, imageSniffBytes, imageTooLargeMessage, sniffImageMimeType, } from "@fungi.computer/knapsack/files-read";
6
+ import { RE2JS } from "re2js";
7
+ import { createPrivateFilesPatch } from "./private-files-patch.js";
8
+ import { fitLocalImage, maximumLocalImageInputBytes, } from "./local-image-fit.js";
9
+ import writeFileAtomic from "write-file-atomic";
10
+ const maximumFileBytes = 128 * 1_024;
11
+ const defaultScanBounds = {
12
+ candidates: 50_000,
13
+ bytes: 64 * 1_024 * 1_024,
14
+ };
15
+ const utf8 = new TextEncoder();
16
+ /** Safe Files path failures surfaced across the workerd binding boundary. */
17
+ export class FilesPathError extends Error {
18
+ reason;
19
+ code;
20
+ constructor(reason, requested) {
21
+ const description = {
22
+ "outside-root": "Files path resolves outside the invocation root",
23
+ "not-found": "Files path does not exist",
24
+ "not-file": "Files path is not a file",
25
+ }[reason];
26
+ super(`${description}: ${requested}`);
27
+ this.reason = reason;
28
+ this.name = "FilesPathError";
29
+ // Optional instruction and skill reads retain Node's missing-file signal.
30
+ this.code = reason === "not-found" ? "ENOENT" : undefined;
31
+ }
32
+ }
33
+ function missingPath(cause) {
34
+ return cause instanceof Error && "code" in cause && cause.code === "ENOENT";
35
+ }
36
+ async function existingRealpath(candidate, requested) {
37
+ try {
38
+ return await realpath(candidate);
39
+ }
40
+ catch (cause) {
41
+ if (missingPath(cause))
42
+ throw new FilesPathError("not-found", requested);
43
+ throw cause;
44
+ }
45
+ }
46
+ /** One explicit bound on walked entries and one on bytes read. */
47
+ class ScanBudget {
48
+ bounds;
49
+ bytes = 0;
50
+ candidates = 0;
51
+ exhausted = false;
52
+ constructor(bounds) {
53
+ this.bounds = bounds;
54
+ }
55
+ admitCandidate() {
56
+ if (this.candidates === this.bounds.candidates) {
57
+ this.exhausted = true;
58
+ return false;
59
+ }
60
+ this.candidates += 1;
61
+ return true;
62
+ }
63
+ admitBytes(bytes) {
64
+ if (bytes > this.bounds.bytes - this.bytes) {
65
+ this.exhausted = true;
66
+ return false;
67
+ }
68
+ this.bytes += bytes;
69
+ return true;
70
+ }
71
+ }
72
+ function checkCancellation(context) {
73
+ context.signal.throwIfAborted();
74
+ }
75
+ // Grep skips unsupported text; filesystem failures are reported separately.
76
+ class FilesTextError extends Error {
77
+ }
78
+ function decodeUtf8(data, path, moreBytesRemain = false) {
79
+ try {
80
+ const content = new TextDecoder("utf-8", {
81
+ fatal: true,
82
+ ignoreBOM: true,
83
+ }).decode(data, { stream: moreBytesRemain });
84
+ const bytes = utf8.encode(content).byteLength;
85
+ if (bytes === 0 && data.byteLength > 0) {
86
+ throw new Error(`Files read cannot advance a UTF-8 page: ${path}`);
87
+ }
88
+ return { content, bytes };
89
+ }
90
+ catch (cause) {
91
+ throw new FilesTextError(`Files path is not valid UTF-8: ${path}`, {
92
+ cause,
93
+ });
94
+ }
95
+ }
96
+ function entryType(metadata, path) {
97
+ if (metadata.isSymbolicLink())
98
+ return "symlink";
99
+ if (metadata.isFile())
100
+ return "file";
101
+ if (metadata.isDirectory())
102
+ return "directory";
103
+ throw new Error(`Files path has an unsupported filesystem type: ${path}`);
104
+ }
105
+ function outputPath(parent, child) {
106
+ if (isAbsolute(parent))
107
+ return resolve(parent, child);
108
+ return parent === "." ? child : normalize(join(parent, child));
109
+ }
110
+ function lineMatcher(pattern, regex, ignoreCase) {
111
+ if (regex) {
112
+ let compiled;
113
+ try {
114
+ compiled = RE2JS.compile(pattern, ignoreCase ? RE2JS.CASE_INSENSITIVE : 0);
115
+ }
116
+ catch (cause) {
117
+ throw new Error("Files grep pattern is not supported by RE2", { cause });
118
+ }
119
+ return (line) => compiled.test(line);
120
+ }
121
+ if (!ignoreCase)
122
+ return (line) => line.includes(pattern);
123
+ const foldedPattern = pattern.toLowerCase();
124
+ return (line) => line.toLowerCase().includes(foldedPattern);
125
+ }
126
+ function appendMatchingLines(matches, path, content, matchesLine, limit, context) {
127
+ const lines = content.split(/\r\n|\n|\r/u);
128
+ if (content.endsWith("\n") || content.endsWith("\r"))
129
+ lines.pop();
130
+ for (const [index, line] of lines.entries()) {
131
+ checkCancellation(context);
132
+ if (!matchesLine(line))
133
+ continue;
134
+ if (matches.length === limit)
135
+ return true;
136
+ matches.push({ path, line: index + 1, text: line });
137
+ }
138
+ return false;
139
+ }
140
+ async function openRegularFile(path, context) {
141
+ checkCancellation(context);
142
+ let handle;
143
+ try {
144
+ handle = await open(path.resolved, "r");
145
+ }
146
+ catch (cause) {
147
+ if (missingPath(cause))
148
+ throw new FilesPathError("not-found", path.requested);
149
+ throw cause;
150
+ }
151
+ try {
152
+ checkCancellation(context);
153
+ const metadata = await handle.stat();
154
+ if (!metadata.isFile())
155
+ throw new FilesPathError("not-file", path.requested);
156
+ return { handle, size: metadata.size };
157
+ }
158
+ catch (cause) {
159
+ await handle.close();
160
+ throw cause;
161
+ }
162
+ }
163
+ /**
164
+ * One canonical invocation root and every pathname decision made beneath it.
165
+ *
166
+ * This is same-user containment, not an OS sandbox against pathname races.
167
+ */
168
+ class InvocationRoot {
169
+ canonicalPath;
170
+ static async capture(path) {
171
+ return new InvocationRoot(await realpath(resolve(path)));
172
+ }
173
+ constructor(canonicalPath) {
174
+ this.canonicalPath = canonicalPath;
175
+ }
176
+ async existing(requested, context) {
177
+ checkCancellation(context);
178
+ const candidate = isAbsolute(requested)
179
+ ? resolve(requested)
180
+ : resolve(this.canonicalPath, requested);
181
+ this.assertContained(candidate, requested);
182
+ const resolvedPath = await existingRealpath(candidate, requested);
183
+ this.assertContained(resolvedPath, requested);
184
+ return { requested, resolved: resolvedPath };
185
+ }
186
+ async walked(candidate, requested, context) {
187
+ checkCancellation(context);
188
+ const resolvedPath = await existingRealpath(candidate, requested);
189
+ this.assertContained(resolvedPath, requested);
190
+ return resolvedPath;
191
+ }
192
+ assertContained(candidate, requested) {
193
+ const fromRoot = relative(this.canonicalPath, candidate);
194
+ if (fromRoot === "" ||
195
+ (!fromRoot.startsWith(`..${sep}`) &&
196
+ fromRoot !== ".." &&
197
+ !isAbsolute(fromRoot))) {
198
+ return;
199
+ }
200
+ throw new FilesPathError("outside-root", requested);
201
+ }
202
+ }
203
+ async function readBytesAt(file, path, position, length, context) {
204
+ const buffer = Buffer.alloc(Math.min(length, file.size - position));
205
+ let bytesRead = 0;
206
+ while (bytesRead < buffer.byteLength) {
207
+ checkCancellation(context);
208
+ const next = await file.handle.read(buffer, bytesRead, buffer.byteLength - bytesRead, position + bytesRead);
209
+ if (next.bytesRead === 0)
210
+ throw new Error(`Files path changed during read: ${path.requested}`);
211
+ bytesRead += next.bytesRead;
212
+ }
213
+ return buffer;
214
+ }
215
+ async function readPage(path, offset, limit, context) {
216
+ const file = await openRegularFile(path, context);
217
+ try {
218
+ if (offset > file.size)
219
+ throw new Error(`Files read offset exceeds file size: ${path.requested}`);
220
+ if (offset === file.size) {
221
+ return {
222
+ path: path.requested,
223
+ content: "",
224
+ offset,
225
+ nextOffset: null,
226
+ truncated: false,
227
+ };
228
+ }
229
+ if (offset === 0) {
230
+ const head = await readBytesAt(file, path, 0, imageSniffBytes, context);
231
+ const mimeType = sniffImageMimeType(head);
232
+ if (mimeType !== undefined) {
233
+ if (file.size > maximumLocalImageInputBytes)
234
+ throw new Error(imageTooLargeMessage(path.requested, file.size, maximumLocalImageInputBytes));
235
+ // Every image is shrunk here, before it becomes an image part.
236
+ return imageReadResult(path.requested, "image/jpeg", await fitLocalImage(path.requested, await readBytesAt(file, path, 0, file.size, context), context.signal));
237
+ }
238
+ }
239
+ const requestedBytes = Math.min(limit, file.size - offset);
240
+ const buffer = await readBytesAt(file, path, offset, requestedBytes, context);
241
+ const bytesRead = buffer.byteLength;
242
+ const page = decodeUtf8(buffer, path.requested, offset + bytesRead < file.size);
243
+ const nextOffset = offset + page.bytes;
244
+ return {
245
+ path: path.requested,
246
+ content: page.content,
247
+ offset,
248
+ nextOffset: nextOffset < file.size ? nextOffset : null,
249
+ truncated: nextOffset < file.size,
250
+ };
251
+ }
252
+ finally {
253
+ await file.handle.close();
254
+ }
255
+ }
256
+ async function readBoundedText(path, context) {
257
+ const file = await openRegularFile(path, context);
258
+ try {
259
+ if (file.size > maximumFileBytes) {
260
+ throw new FilesTextError(`Files path exceeds the ${String(maximumFileBytes)}-byte bound: ${path.requested}`);
261
+ }
262
+ const buffer = Buffer.alloc(maximumFileBytes + 1);
263
+ let bytesRead = 0;
264
+ while (bytesRead < buffer.byteLength) {
265
+ checkCancellation(context);
266
+ const next = await file.handle.read(buffer, bytesRead, buffer.byteLength - bytesRead, bytesRead);
267
+ if (next.bytesRead === 0)
268
+ break;
269
+ bytesRead += next.bytesRead;
270
+ }
271
+ if (bytesRead > maximumFileBytes) {
272
+ throw new FilesTextError(`Files path exceeds the ${String(maximumFileBytes)}-byte bound: ${path.requested}`);
273
+ }
274
+ return decodeUtf8(buffer.subarray(0, bytesRead), path.requested).content;
275
+ }
276
+ finally {
277
+ await file.handle.close();
278
+ }
279
+ }
280
+ async function readGrepCandidate(path, context) {
281
+ try {
282
+ return await readBoundedText(path, context);
283
+ }
284
+ catch (cause) {
285
+ checkCancellation(context);
286
+ if (cause instanceof FilesTextError)
287
+ return undefined;
288
+ console.error("Local file search candidate unavailable", cause);
289
+ return undefined;
290
+ }
291
+ }
292
+ function slashed(path) {
293
+ return path.split(sep).join("/");
294
+ }
295
+ async function readGitignore(root, directory, context) {
296
+ const candidate = join(directory, ".gitignore");
297
+ try {
298
+ await lstat(candidate);
299
+ }
300
+ catch (cause) {
301
+ if (missingPath(cause))
302
+ return undefined;
303
+ throw cause;
304
+ }
305
+ let resolved;
306
+ try {
307
+ resolved = await root.walked(candidate, candidate, context);
308
+ }
309
+ catch (cause) {
310
+ if (cause instanceof FilesPathError)
311
+ return undefined;
312
+ throw cause;
313
+ }
314
+ return await readGrepCandidate({ requested: candidate, resolved }, context);
315
+ }
316
+ function visiblePath(root, visibility, path, isDirectory) {
317
+ return visibility.visible(slashed(relative(root.canonicalPath, path)), isDirectory);
318
+ }
319
+ async function* walkFiles(root, base, budget, visibility, context) {
320
+ async function* visit(directory, relativeParent, ancestors) {
321
+ if (ancestors.has(directory))
322
+ return;
323
+ const childAncestors = new Set(ancestors).add(directory);
324
+ checkCancellation(context);
325
+ const handle = await opendir(directory);
326
+ for await (const entry of handle) {
327
+ checkCancellation(context);
328
+ const candidate = join(directory, entry.name);
329
+ if (!(await visiblePath(root, visibility, candidate, entry.isDirectory())))
330
+ continue;
331
+ if (!budget.admitCandidate())
332
+ return;
333
+ const relativePath = relativeParent === "" ? entry.name : join(relativeParent, entry.name);
334
+ const resolvedPath = await root.walked(candidate, outputPath(base.requested, relativePath), context);
335
+ checkCancellation(context);
336
+ const metadata = await stat(resolvedPath);
337
+ const walkedEntry = {
338
+ relativePath,
339
+ resolvedPath,
340
+ isDirectory: metadata.isDirectory(),
341
+ isFile: metadata.isFile(),
342
+ size: metadata.size,
343
+ };
344
+ yield walkedEntry;
345
+ if (walkedEntry.isDirectory) {
346
+ yield* visit(resolvedPath, relativePath, childAncestors);
347
+ if (budget.exhausted)
348
+ return;
349
+ }
350
+ }
351
+ }
352
+ yield* visit(base.resolved, "", new Set());
353
+ }
354
+ async function grepFile(target, input, context) {
355
+ const matches = [];
356
+ const content = await readBoundedText(target, context);
357
+ const limited = appendMatchingLines(matches, input.path, content, lineMatcher(input.pattern, input.regex, input.ignoreCase), input.limit, context);
358
+ return {
359
+ path: input.path,
360
+ results: matches,
361
+ truncated: limited ? "limit" : null,
362
+ };
363
+ }
364
+ /** One directory level; the entries past the limit are only witnessed, not returned. */
365
+ async function listLevel(root, directory, input, visibility, context) {
366
+ const handle = await opendir(directory.resolved);
367
+ const results = [];
368
+ for await (const entry of handle) {
369
+ checkCancellation(context);
370
+ if (!(await visiblePath(root, visibility, join(directory.resolved, entry.name), entry.isDirectory())))
371
+ continue;
372
+ if (results.length === input.limit) {
373
+ return { path: input.path, results, truncated: "limit" };
374
+ }
375
+ const metadata = await lstat(join(directory.resolved, entry.name));
376
+ const path = outputPath(input.path, entry.name);
377
+ results.push({ path, type: entryType(metadata, path) });
378
+ }
379
+ return { path: input.path, results, truncated: null };
380
+ }
381
+ /** One bounded walk for a glob or content search with an optional include glob. */
382
+ async function searchTree(root, directory, search, bounds, visibility, context) {
383
+ const { input } = search;
384
+ const pathPattern = search.kind === "glob" ? search.input.pattern : search.input.include;
385
+ const matchesLine = search.kind === "grep"
386
+ ? lineMatcher(search.input.pattern, search.input.regex, search.input.ignoreCase)
387
+ : undefined;
388
+ const results = [];
389
+ const budget = new ScanBudget(bounds);
390
+ for await (const entry of walkFiles(root, directory, budget, visibility, context)) {
391
+ checkCancellation(context);
392
+ if (pathPattern !== undefined &&
393
+ !matchesGlob(entry.relativePath, pathPattern))
394
+ continue;
395
+ const path = outputPath(input.path, entry.relativePath);
396
+ if (matchesLine === undefined) {
397
+ if (results.length === input.limit) {
398
+ return { path: input.path, results, truncated: "limit" };
399
+ }
400
+ results.push({
401
+ path,
402
+ type: entry.isDirectory ? "directory" : "file",
403
+ });
404
+ continue;
405
+ }
406
+ // Directories, special files, oversized files and non-text files hold no
407
+ // searchable lines; skipping them is not a cut of the scan.
408
+ if (!entry.isFile || entry.size > maximumFileBytes)
409
+ continue;
410
+ if (!budget.admitBytes(entry.size))
411
+ break;
412
+ const content = await readGrepCandidate({ requested: path, resolved: entry.resolvedPath }, context);
413
+ if (content === undefined)
414
+ continue;
415
+ if (appendMatchingLines(results, path, content, matchesLine, input.limit, context)) {
416
+ return { path: input.path, results, truncated: "limit" };
417
+ }
418
+ }
419
+ return {
420
+ path: input.path,
421
+ results,
422
+ truncated: budget.exhausted ? "scan" : null,
423
+ };
424
+ }
425
+ async function searchPath(root, search, bounds, context) {
426
+ const { input } = search;
427
+ const target = await root.existing(input.path, context);
428
+ checkCancellation(context);
429
+ const metadata = await stat(target.resolved);
430
+ const visibility = new SearchVisibility((directory) => readGitignore(root, join(root.canonicalPath, directory), context));
431
+ if (!(await visiblePath(root, visibility, target.resolved, metadata.isDirectory())))
432
+ return { path: input.path, results: [], truncated: null };
433
+ if (metadata.isFile()) {
434
+ if (search.kind === "glob")
435
+ throw new Error(`Files glob needs a directory path, not a file: ${input.path}`);
436
+ if (search.kind === "grep") {
437
+ if (search.input.include !== undefined)
438
+ throw new Error(`Files grep include needs a directory path, not a file: ${input.path}`);
439
+ return await grepFile(target, search.input, context);
440
+ }
441
+ return {
442
+ path: input.path,
443
+ results: [{ path: input.path, type: "file" }],
444
+ truncated: null,
445
+ };
446
+ }
447
+ if (!metadata.isDirectory())
448
+ throw new Error(`Files path is not a file or directory: ${input.path}`);
449
+ if (search.kind === "list")
450
+ return await listLevel(root, target, input, visibility, context);
451
+ return await searchTree(root, target, search, bounds, visibility, context);
452
+ }
453
+ function createWriteQueue() {
454
+ let tail = Promise.resolve();
455
+ return async (operation) => {
456
+ const previous = tail;
457
+ const gate = Promise.withResolvers();
458
+ tail = previous.then(() => gate.promise);
459
+ await previous;
460
+ try {
461
+ return await operation();
462
+ }
463
+ finally {
464
+ gate.resolve(undefined);
465
+ }
466
+ };
467
+ }
468
+ /**
469
+ * Construct the private local Files port for one captured invocation root.
470
+ *
471
+ * Resolved paths stay under the captured canonical root, including admitted
472
+ * in-root symlinks. This is trusted same-user containment, not protection from
473
+ * a same-user process racing pathname resolution.
474
+ */
475
+ export async function createPrivateInvocationFilesPort(path, scanBounds = defaultScanBounds, sandbox) {
476
+ const root = await InvocationRoot.capture(path);
477
+ const write = createWriteQueue();
478
+ const port = {
479
+ async read(input, context) {
480
+ return await readPage(await root.existing(input.path, context), input.offset, input.limit, context);
481
+ },
482
+ async list(input, context) {
483
+ return await searchPath(root, { kind: "list", input }, scanBounds, context);
484
+ },
485
+ async glob(input, context) {
486
+ return await searchPath(root, { kind: "glob", input }, scanBounds, context);
487
+ },
488
+ async grep(input, context) {
489
+ return await searchPath(root, { kind: "grep", input }, scanBounds, context);
490
+ },
491
+ async replace(input, context) {
492
+ return await write(async () => {
493
+ checkCancellation(context);
494
+ const path = await root.existing(input.path, context);
495
+ const current = await readBoundedText(path, context);
496
+ if (current !== input.expectedContent)
497
+ throw new FilesEditError("conflict", input.path, "File changed since it was read; read it again before editing");
498
+ const bytes = utf8.encode(input.content).byteLength;
499
+ if (bytes > maximumFileBytes) {
500
+ throw new FilesEditError("file-too-large", input.path, `Result exceeds ${String(maximumFileBytes)} bytes; use files.patch for a small change`);
501
+ }
502
+ checkCancellation(context);
503
+ sandbox?.assertWrite(path.resolved);
504
+ await writeFileAtomic(path.resolved, input.content, {
505
+ encoding: "utf8",
506
+ });
507
+ return { path: input.path, bytes };
508
+ });
509
+ },
510
+ };
511
+ // The native parser/patch engine still owns patch grammar. Its adapter must
512
+ // accept the launch callback too; never parse patch headers here.
513
+ const patch = await createPrivateFilesPatch(root.canonicalPath, port.read, sandbox?.launch);
514
+ if (patch === undefined)
515
+ return Object.freeze(port);
516
+ return Object.freeze({
517
+ ...port,
518
+ patch: (input, context) => write(() => patch(input, context)),
519
+ });
520
+ }
@@ -0,0 +1,17 @@
1
+ import * as acp from "@agentclientprotocol/sdk";
2
+ import type { ShiitakeEventSourceConstructor } from "@fungi.computer/shiitake/client";
3
+ import { type PrivateLocalServiceManager } from "./private-local-service.js";
4
+ export type LocalAcpStdioOptions = Readonly<{
5
+ serviceDirectory?: string;
6
+ invocationRoot?: string;
7
+ databasePath?: string;
8
+ eventSource?: ShiitakeEventSourceConstructor;
9
+ manager?: PrivateLocalServiceManager;
10
+ input?: ReadableStream<Uint8Array>;
11
+ output?: WritableStream<Uint8Array>;
12
+ name?: string;
13
+ }>;
14
+ /** Build the ACP agent half over the persistent Local service's Session client. */
15
+ export declare function createPrivateLocalAcpAgent(options?: LocalAcpStdioOptions): acp.AgentApp;
16
+ /** Serve one ACP stable-v1 stdio connection over the persistent Local service. */
17
+ export declare function runPrivateLocalAcpStdio(options?: LocalAcpStdioOptions): Promise<void>;
@@ -0,0 +1,145 @@
1
+ import * as acp from "@agentclientprotocol/sdk";
2
+ import * as Schema from "effect/Schema";
3
+ import { createShiitakeAcpAgent, } from "@fungi.computer/graft";
4
+ import { createPrivateLocalServiceManager, } from "./private-local-service.js";
5
+ import { createPrivateLocalSessionRequestId } from "./private-local-runtime-host.js";
6
+ function stdinByteStream() {
7
+ const input = process.stdin[Symbol.asyncIterator]();
8
+ const encoder = new TextEncoder();
9
+ const chunkSchema = Schema.Union([Schema.Uint8Array, Schema.String]);
10
+ return new ReadableStream({
11
+ async pull(controller) {
12
+ try {
13
+ const next = await input.next();
14
+ if (next.done) {
15
+ controller.close();
16
+ }
17
+ else {
18
+ const chunk = Schema.decodeUnknownSync(chunkSchema)(next.value);
19
+ controller.enqueue(Schema.is(Schema.Uint8Array)(chunk) ? chunk : encoder.encode(chunk));
20
+ }
21
+ }
22
+ catch (cause) {
23
+ controller.error(cause);
24
+ }
25
+ },
26
+ async cancel() {
27
+ await input.return?.();
28
+ },
29
+ });
30
+ }
31
+ function stdoutByteStream() {
32
+ return new WritableStream({
33
+ write(chunk) {
34
+ return new Promise((resolve, reject) => {
35
+ process.stdout.write(chunk, (error) => {
36
+ if (error)
37
+ reject(error);
38
+ else
39
+ resolve();
40
+ });
41
+ });
42
+ },
43
+ close() {
44
+ process.stdout.end();
45
+ },
46
+ abort(reason) {
47
+ process.stdout.destroy(reason instanceof Error
48
+ ? reason
49
+ : new Error("ACP stdout was aborted", { cause: reason }));
50
+ },
51
+ });
52
+ }
53
+ function assertLocalInputs(additionalDirectories, mcpServers) {
54
+ if (additionalDirectories.length > 0)
55
+ throw new Error("Local ACP does not support ACP additionalDirectories yet");
56
+ if (mcpServers.length > 0)
57
+ throw new Error("Local ACP does not support ACP MCP servers yet");
58
+ }
59
+ function binding(connection) {
60
+ return {
61
+ session: connection.session,
62
+ sessionId: connection.sessionId,
63
+ close: connection.close,
64
+ };
65
+ }
66
+ function localDirectory(manager, databasePath, invocationRoot) {
67
+ return {
68
+ async list(input) {
69
+ if (input.cursor !== undefined)
70
+ throw acp.RequestError.invalidParams(undefined, "Local discovery has no next page");
71
+ const cwd = input.cwd ?? invocationRoot;
72
+ const discovery = await manager.discover(cwd, { signal: input.signal });
73
+ if (discovery.problems.length > 0)
74
+ throw new Error("Local Session discovery contains unreadable stores");
75
+ return {
76
+ sessions: discovery.sessions.map((session) => ({
77
+ sessionId: session.sessionId,
78
+ cwd,
79
+ title: session.title,
80
+ updatedAt: new Date(session.updatedAt).toISOString(),
81
+ })),
82
+ };
83
+ },
84
+ async create(input) {
85
+ assertLocalInputs(input.additionalDirectories, input.mcpServers);
86
+ const selected = await manager.connect({
87
+ invocationRoot: input.cwd,
88
+ databasePath,
89
+ uiAttachmentLifetime: "connection",
90
+ }, { signal: input.signal });
91
+ try {
92
+ const created = await manager.createSession({
93
+ invocationRoot: input.cwd,
94
+ databasePath: selected.databasePath,
95
+ requestId: createPrivateLocalSessionRequestId(),
96
+ uiAttachmentLifetime: "connection",
97
+ }, { signal: input.signal });
98
+ return binding(created);
99
+ }
100
+ finally {
101
+ await selected.close();
102
+ }
103
+ },
104
+ async load(input) {
105
+ assertLocalInputs(input.additionalDirectories, input.mcpServers);
106
+ const connected = await manager.connect({
107
+ invocationRoot: input.cwd,
108
+ databasePath,
109
+ sessionId: input.sessionId,
110
+ uiAttachmentLifetime: "connection",
111
+ }, { signal: input.signal });
112
+ return binding(connected);
113
+ },
114
+ };
115
+ }
116
+ /** Build the ACP agent half over the persistent Local service's Session client. */
117
+ export function createPrivateLocalAcpAgent(options = {}) {
118
+ const manager = options.manager ??
119
+ createPrivateLocalServiceManager(localManagerOptions(options));
120
+ return createShiitakeAcpAgent({
121
+ directory: localDirectory(manager, options.databasePath, options.invocationRoot ?? process.cwd()),
122
+ terminalAuth: {
123
+ id: "fungi-login",
124
+ name: "Sign in to Fungi",
125
+ args: ["login"],
126
+ },
127
+ name: options.name ?? "Local Shiitake",
128
+ });
129
+ }
130
+ function localManagerOptions(options) {
131
+ const result = {};
132
+ if (options.serviceDirectory !== undefined)
133
+ result.directory = options.serviceDirectory;
134
+ if (options.eventSource !== undefined)
135
+ result.eventSource = options.eventSource;
136
+ return result;
137
+ }
138
+ /** Serve one ACP stable-v1 stdio connection over the persistent Local service. */
139
+ export async function runPrivateLocalAcpStdio(options = {}) {
140
+ const app = createPrivateLocalAcpAgent(options);
141
+ const input = options.input ?? stdinByteStream();
142
+ const output = options.output ?? stdoutByteStream();
143
+ const connection = app.connect(acp.ndJsonStream(output, input));
144
+ await connection.closed;
145
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Admissions of work to one Agent share it as short leases; an archive takes the
3
+ * Agent exclusively. So an archive sees every command admitted before it, and
4
+ * work offered after it finds the Agent archived: nothing runs in between.
5
+ */
6
+ export declare class AdmissionGate {
7
+ #private;
8
+ /** Nothing holds or waits for this gate, so it can be forgotten. */
9
+ get idle(): boolean;
10
+ /** Hold a shared lease; the returned release may be called more than once. */
11
+ lease(): Promise<() => void>;
12
+ /** Run alone: after every held lease is released and before any new one. */
13
+ exclusive<Value>(run: () => Promise<Value>): Promise<Value>;
14
+ }