@fungi.computer/truffle 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +294 -2
- package/computerd-loopback.cjs +16 -0
- package/dist/auth-cli.d.ts +36 -0
- package/dist/auth-cli.js +351 -0
- package/dist/fungi-account-command.d.ts +18 -0
- package/dist/fungi-account-command.js +92 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +17 -0
- package/dist/internal/private-workerd-sandbox-worker.mjs +27488 -0
- package/dist/internal/sandbox-metafile.json +6726 -0
- package/dist/linear-auth.d.ts +4 -0
- package/dist/linear-auth.js +72 -0
- package/dist/local-extensions.d.ts +97 -0
- package/dist/local-extensions.js +484 -0
- package/dist/local-login.d.ts +55 -0
- package/dist/local-login.js +167 -0
- package/dist/local-models.d.ts +14 -0
- package/dist/local-models.js +77 -0
- package/dist/local-skill-source.d.ts +6 -0
- package/dist/local-skill-source.js +357 -0
- package/dist/log-event.d.ts +2 -0
- package/dist/log-event.js +9 -0
- package/dist/private-bun-shiitake-host.d.ts +11 -0
- package/dist/private-bun-shiitake-host.js +66 -0
- package/dist/private-file-credential-store.d.ts +33 -0
- package/dist/private-file-credential-store.js +283 -0
- package/dist/private-files-patch.d.ts +3 -0
- package/dist/private-files-patch.js +86 -0
- package/dist/private-fungi-account.d.ts +235 -0
- package/dist/private-fungi-account.js +1149 -0
- package/dist/private-fungi-auth-error.d.ts +5 -0
- package/dist/private-fungi-auth-error.js +7 -0
- package/dist/private-fungi-mcp-connection.d.ts +22 -0
- package/dist/private-fungi-mcp-connection.js +143 -0
- package/dist/private-fungi-mcp-endpoint.d.ts +2 -0
- package/dist/private-fungi-mcp-endpoint.js +15 -0
- package/dist/private-fungi-mcp-selection.d.ts +14 -0
- package/dist/private-fungi-mcp-selection.js +36 -0
- package/dist/private-fungi-model-api.d.ts +6 -0
- package/dist/private-fungi-model-api.js +125 -0
- package/dist/private-fungi-model-catalog.d.ts +6 -0
- package/dist/private-fungi-model-catalog.js +78 -0
- package/dist/private-fungi-model-provider.d.ts +6 -0
- package/dist/private-fungi-model-provider.js +46 -0
- package/dist/private-fungi-pending-logouts.d.ts +34 -0
- package/dist/private-fungi-pending-logouts.js +136 -0
- package/dist/private-invocation-computer.d.ts +37 -0
- package/dist/private-invocation-computer.js +459 -0
- package/dist/private-invocation-files.d.ts +15 -0
- package/dist/private-invocation-files.js +449 -0
- package/dist/private-local-acp-stdio.d.ts +17 -0
- package/dist/private-local-acp-stdio.js +142 -0
- package/dist/private-local-auth-rpc.d.ts +69 -0
- package/dist/private-local-auth-rpc.js +273 -0
- package/dist/private-local-computer-attach.d.ts +49 -0
- package/dist/private-local-computer-attach.js +216 -0
- package/dist/private-local-computer-backend.d.ts +282 -0
- package/dist/private-local-computer-backend.js +402 -0
- package/dist/private-local-computer-processes.d.ts +199 -0
- package/dist/private-local-computer-processes.js +589 -0
- package/dist/private-local-default-model.d.ts +11 -0
- package/dist/private-local-default-model.js +45 -0
- package/dist/private-local-delegation.d.ts +29 -0
- package/dist/private-local-delegation.js +78 -0
- package/dist/private-local-effective-model.d.ts +19 -0
- package/dist/private-local-effective-model.js +14 -0
- package/dist/private-local-exec-launch.d.ts +41 -0
- package/dist/private-local-exec-launch.js +51 -0
- package/dist/private-local-exec-path.d.ts +6 -0
- package/dist/private-local-exec-path.js +40 -0
- package/dist/private-local-git-exclude.d.ts +2 -0
- package/dist/private-local-git-exclude.js +33 -0
- package/dist/private-local-http-dispatch.d.ts +5 -0
- package/dist/private-local-http-dispatch.js +14 -0
- package/dist/private-local-mail.d.ts +11 -0
- package/dist/private-local-mail.js +69 -0
- package/dist/private-local-model-settings.d.ts +5 -0
- package/dist/private-local-model-settings.js +31 -0
- package/dist/private-local-runtime-host.d.ts +141 -0
- package/dist/private-local-runtime-host.js +988 -0
- package/dist/private-local-service-contract.d.ts +90 -0
- package/dist/private-local-service-contract.js +1 -0
- package/dist/private-local-service-discovery.d.ts +101 -0
- package/dist/private-local-service-discovery.js +221 -0
- package/dist/private-local-service-ids.d.ts +3 -0
- package/dist/private-local-service-ids.js +8 -0
- package/dist/private-local-service-listener.d.ts +15 -0
- package/dist/private-local-service-listener.js +211 -0
- package/dist/private-local-service-owner.d.ts +1567 -0
- package/dist/private-local-service-owner.js +371 -0
- package/dist/private-local-service-process.d.ts +1 -0
- package/dist/private-local-service-process.js +6 -0
- package/dist/private-local-service.d.ts +17 -0
- package/dist/private-local-service.js +330 -0
- package/dist/private-local-session-discovery.d.ts +19 -0
- package/dist/private-local-session-discovery.js +157 -0
- package/dist/private-local-shiitake-host.d.ts +44 -0
- package/dist/private-local-shiitake-host.js +143 -0
- package/dist/private-local-subagents.d.ts +27 -0
- package/dist/private-local-subagents.js +77 -0
- package/dist/private-local-worker-server.d.ts +15 -0
- package/dist/private-local-worker-server.js +136 -0
- package/dist/private-sqlite-owner.d.ts +7 -0
- package/dist/private-sqlite-owner.js +103 -0
- package/dist/private-team-computers.d.ts +38 -0
- package/dist/private-team-computers.js +101 -0
- package/dist/private-workerd-sandbox.d.ts +3 -0
- package/dist/private-workerd-sandbox.js +347 -0
- package/dist/team-apps.d.ts +24 -0
- package/dist/team-apps.js +123 -0
- package/dist/tracker-git-credential.d.ts +15 -0
- package/dist/tracker-git-credential.js +87 -0
- package/native/bin/build.json +4037 -0
- package/native/bin/fungi-apply-patch +0 -0
- package/package.json +148 -3
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { sandboxBindingNames } from "@fungi.computer/mycelium/sandbox-guest";
|
|
2
|
+
import { recoverSandboxFailure, sandboxTransportMessage, sandboxCapabilityFailure, sandboxTransportErrno, } from "@fungi.computer/mycelium/sandbox-boundary";
|
|
3
|
+
import { nanoid } from "@fungi.computer/cairn";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { request as httpRequest } from "node:http";
|
|
6
|
+
import { SandboxCrashed, DispatchLost, BadResponse, Cancelled, sandboxFailure, } from "@fungi.computer/mycelium";
|
|
7
|
+
import { FilesEditError } from "@fungi.computer/knapsack";
|
|
8
|
+
import { convertV4MiniflareOptions, Miniflare, Response as MiniflareResponse, } from "miniflare";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { sandboxWireSchema, sandboxWireFailure, } from "@fungi.computer/mycelium/sandbox-wire";
|
|
11
|
+
import { FilesPathError } from "./private-invocation-files.js";
|
|
12
|
+
const jsonValueSchema = z.json();
|
|
13
|
+
const bindingCallSchema = z.strictObject({
|
|
14
|
+
namespace: z.string().max(160),
|
|
15
|
+
operation: z.string().max(160),
|
|
16
|
+
input: jsonValueSchema,
|
|
17
|
+
});
|
|
18
|
+
const bindingErrorSchema = z.discriminatedUnion("code", [
|
|
19
|
+
z.strictObject({
|
|
20
|
+
code: z.literal("no_such_operation"),
|
|
21
|
+
path: z.string().max(321),
|
|
22
|
+
suggestions: z.array(z.string().max(160)).max(3),
|
|
23
|
+
}),
|
|
24
|
+
z.strictObject({
|
|
25
|
+
code: z.enum(["invalid_input", "operation_failed", "closing"]),
|
|
26
|
+
message: z.string().max(512),
|
|
27
|
+
}),
|
|
28
|
+
]);
|
|
29
|
+
const bindingResponseSchema = z.discriminatedUnion("ok", [
|
|
30
|
+
z.strictObject({ ok: z.literal(true), value: jsonValueSchema }),
|
|
31
|
+
z.strictObject({ ok: z.literal(false), error: bindingErrorSchema }),
|
|
32
|
+
]);
|
|
33
|
+
const workerBundle = readFile(new URL("./internal/private-workerd-sandbox-worker.mjs", import.meta.url), "utf8");
|
|
34
|
+
// Files owns admission of its diagnostics; the shared boundary alone decodes rejections.
|
|
35
|
+
const filesDiagnosticSchema = z
|
|
36
|
+
.union([z.instanceof(FilesPathError), z.instanceof(FilesEditError)])
|
|
37
|
+
.transform((error) => ({ kind: "Files", message: error.message }));
|
|
38
|
+
function operationTokens(path) {
|
|
39
|
+
return path
|
|
40
|
+
.toLowerCase()
|
|
41
|
+
.split(/[^a-z0-9]+/u)
|
|
42
|
+
.filter(Boolean);
|
|
43
|
+
}
|
|
44
|
+
function commonPrefixLength(left, right) {
|
|
45
|
+
let index = 0;
|
|
46
|
+
while (index < left.length &&
|
|
47
|
+
index < right.length &&
|
|
48
|
+
left[index] === right[index])
|
|
49
|
+
index += 1;
|
|
50
|
+
return index;
|
|
51
|
+
}
|
|
52
|
+
function oneEditApart(left, right) {
|
|
53
|
+
if (Math.abs(left.length - right.length) > 1)
|
|
54
|
+
return false;
|
|
55
|
+
let leftIndex = 0;
|
|
56
|
+
let rightIndex = 0;
|
|
57
|
+
let changed = false;
|
|
58
|
+
while (leftIndex < left.length && rightIndex < right.length) {
|
|
59
|
+
if (left[leftIndex] === right[rightIndex]) {
|
|
60
|
+
leftIndex += 1;
|
|
61
|
+
rightIndex += 1;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (changed)
|
|
65
|
+
return false;
|
|
66
|
+
changed = true;
|
|
67
|
+
if (left.length >= right.length)
|
|
68
|
+
leftIndex += 1;
|
|
69
|
+
if (right.length >= left.length)
|
|
70
|
+
rightIndex += 1;
|
|
71
|
+
}
|
|
72
|
+
return changed || left.length !== right.length;
|
|
73
|
+
}
|
|
74
|
+
function operationSuggestions(request, operation) {
|
|
75
|
+
const requestedTokens = operationTokens(operation);
|
|
76
|
+
const scored = Object.entries(request.bindings).flatMap(([namespace, methods]) => Object.keys(methods).map((method) => {
|
|
77
|
+
const path = `${namespace}.${method}`;
|
|
78
|
+
const candidateTokens = operationTokens(path);
|
|
79
|
+
const score = requestedTokens.reduce((total, requested) => total +
|
|
80
|
+
Math.max(0, ...candidateTokens.map((candidate) => candidate === requested
|
|
81
|
+
? 4
|
|
82
|
+
: oneEditApart(candidate, requested)
|
|
83
|
+
? 3
|
|
84
|
+
: commonPrefixLength(candidate, requested) >= 3
|
|
85
|
+
? 2
|
|
86
|
+
: 0)), 0);
|
|
87
|
+
return { path, score };
|
|
88
|
+
}));
|
|
89
|
+
return scored
|
|
90
|
+
.filter(({ score }) => score > 0)
|
|
91
|
+
.sort((left, right) => right.score - left.score || left.path.localeCompare(right.path))
|
|
92
|
+
.slice(0, 3)
|
|
93
|
+
.map(({ path }) => path);
|
|
94
|
+
}
|
|
95
|
+
function jsonResponse(value, status = 200) {
|
|
96
|
+
return new MiniflareResponse(JSON.stringify(bindingResponseSchema.parse(value)), {
|
|
97
|
+
status,
|
|
98
|
+
headers: { "content-type": "application/json" },
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
async function invokeBinding(request, bindingRequest, failures) {
|
|
102
|
+
let call;
|
|
103
|
+
try {
|
|
104
|
+
call = bindingCallSchema.parse(await bindingRequest.json());
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return jsonResponse({
|
|
108
|
+
ok: false,
|
|
109
|
+
error: { code: "invalid_input", message: "Invalid binding call" },
|
|
110
|
+
}, 400);
|
|
111
|
+
}
|
|
112
|
+
const operation = request.bindings[call.namespace]?.[call.operation];
|
|
113
|
+
if (operation === undefined) {
|
|
114
|
+
return jsonResponse({
|
|
115
|
+
ok: false,
|
|
116
|
+
error: {
|
|
117
|
+
code: "no_such_operation",
|
|
118
|
+
path: `${call.namespace}.${call.operation}`,
|
|
119
|
+
suggestions: operationSuggestions(request, call.operation),
|
|
120
|
+
},
|
|
121
|
+
}, 404);
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
const value = jsonValueSchema.parse(await operation(call.input));
|
|
125
|
+
return jsonResponse({ ok: true, value });
|
|
126
|
+
}
|
|
127
|
+
catch (cause) {
|
|
128
|
+
const failure = sandboxCapabilityFailure(cause, call.namespace, call.operation, filesDiagnosticSchema);
|
|
129
|
+
const ref = nanoid();
|
|
130
|
+
if (failures.size === 128)
|
|
131
|
+
failures.delete(failures.keys().next().value ?? "");
|
|
132
|
+
failures.set(ref, failure);
|
|
133
|
+
return jsonResponse({
|
|
134
|
+
ok: false,
|
|
135
|
+
error: {
|
|
136
|
+
code: failure._tag === "CapabilityFailed" &&
|
|
137
|
+
failure.cause.kind === "InvalidInput"
|
|
138
|
+
? "invalid_input"
|
|
139
|
+
: "operation_failed",
|
|
140
|
+
message: sandboxTransportMessage(failure, ref),
|
|
141
|
+
},
|
|
142
|
+
}, 400);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function createInvocationCallbacks(request) {
|
|
146
|
+
const failures = new Map();
|
|
147
|
+
const activeCallbacks = new Set();
|
|
148
|
+
let acceptingCallbacks = true;
|
|
149
|
+
const fetch = (bindingRequest) => {
|
|
150
|
+
if (!acceptingCallbacks) {
|
|
151
|
+
return Promise.resolve(jsonResponse({
|
|
152
|
+
ok: false,
|
|
153
|
+
error: { code: "closing", message: "Invocation is closing" },
|
|
154
|
+
}, 503));
|
|
155
|
+
}
|
|
156
|
+
const callback = invokeBinding(request, bindingRequest, failures);
|
|
157
|
+
activeCallbacks.add(callback);
|
|
158
|
+
void callback.then(() => activeCallbacks.delete(callback), () => activeCallbacks.delete(callback));
|
|
159
|
+
return callback;
|
|
160
|
+
};
|
|
161
|
+
return Object.freeze({
|
|
162
|
+
failures,
|
|
163
|
+
fetch,
|
|
164
|
+
closeAdmission: () => {
|
|
165
|
+
acceptingCallbacks = false;
|
|
166
|
+
},
|
|
167
|
+
drain: async () => {
|
|
168
|
+
await Promise.allSettled(activeCallbacks);
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
function createInvocationMiniflare(bundle, serviceBinding) {
|
|
173
|
+
return new Miniflare(convertV4MiniflareOptions({
|
|
174
|
+
modules: [
|
|
175
|
+
{
|
|
176
|
+
type: "ESModule",
|
|
177
|
+
path: "private-workerd-sandbox-worker.mjs",
|
|
178
|
+
contents: bundle,
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
compatibilityDate: "2026-09-02",
|
|
182
|
+
compatibilityFlags: ["nodejs_compat"],
|
|
183
|
+
workerLoaders: { LOADER: {} },
|
|
184
|
+
serviceBindings: { INVOCATION: serviceBinding },
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
187
|
+
async function dispatchInvocation(miniflare, request, signal) {
|
|
188
|
+
let response;
|
|
189
|
+
const startedAt = performance.now();
|
|
190
|
+
let startedDispatch = false;
|
|
191
|
+
try {
|
|
192
|
+
// Bun's built-in undici Pool ignores Miniflare's zero-timeout settings;
|
|
193
|
+
// dispatchFetch inherits Bun.fetch's 300 s idle timer. Use the public
|
|
194
|
+
// workerd listener with node:http, whose request has no implicit deadline.
|
|
195
|
+
// Mycelium's signal remains the invocation's cancellation authority.
|
|
196
|
+
const url = new URL("/execute", await miniflare.ready);
|
|
197
|
+
startedDispatch = true;
|
|
198
|
+
response = await new Promise((resolve, reject) => {
|
|
199
|
+
const outgoing = httpRequest(url, {
|
|
200
|
+
method: "POST",
|
|
201
|
+
headers: { "content-type": "application/json" },
|
|
202
|
+
agent: false,
|
|
203
|
+
signal,
|
|
204
|
+
}, (incoming) => {
|
|
205
|
+
resolve(new MiniflareResponse(incoming, {
|
|
206
|
+
status: incoming.statusCode ?? 500,
|
|
207
|
+
}));
|
|
208
|
+
});
|
|
209
|
+
outgoing.on("error", reject);
|
|
210
|
+
outgoing.end(JSON.stringify({
|
|
211
|
+
code: request.code,
|
|
212
|
+
timeoutMs: request.timeoutMs,
|
|
213
|
+
bindings: sandboxBindingNames(request.bindings),
|
|
214
|
+
namespacePaths: request.namespacePaths ?? {},
|
|
215
|
+
}));
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
catch (cause) {
|
|
219
|
+
return {
|
|
220
|
+
ok: false,
|
|
221
|
+
cause: signal.aborted
|
|
222
|
+
? new Cancelled({ by: "caller" })
|
|
223
|
+
: startedDispatch
|
|
224
|
+
? new DispatchLost({
|
|
225
|
+
elapsedMs: Math.round(performance.now() - startedAt),
|
|
226
|
+
transport: "node:http",
|
|
227
|
+
errno: sandboxTransportErrno(cause),
|
|
228
|
+
rawCause: cause,
|
|
229
|
+
})
|
|
230
|
+
: new SandboxCrashed({ rawCause: cause }),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
let payload;
|
|
234
|
+
try {
|
|
235
|
+
payload = await response.json();
|
|
236
|
+
}
|
|
237
|
+
catch (cause) {
|
|
238
|
+
return {
|
|
239
|
+
ok: false,
|
|
240
|
+
cause: new BadResponse({
|
|
241
|
+
reason: "response is not JSON",
|
|
242
|
+
rawCause: cause,
|
|
243
|
+
}),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
const parsed = sandboxWireSchema.safeParse(payload);
|
|
247
|
+
if (!parsed.success) {
|
|
248
|
+
return {
|
|
249
|
+
ok: false,
|
|
250
|
+
cause: new BadResponse({
|
|
251
|
+
reason: "response does not match sandbox envelope",
|
|
252
|
+
rawCause: parsed.error,
|
|
253
|
+
}),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
const envelope = parsed.data;
|
|
257
|
+
return envelope.ok
|
|
258
|
+
? { ok: true, value: envelope.value }
|
|
259
|
+
: {
|
|
260
|
+
ok: false,
|
|
261
|
+
cause: sandboxWireFailure(envelope.error),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function createDisposal(miniflare, callbacks) {
|
|
265
|
+
let disposal;
|
|
266
|
+
return () => {
|
|
267
|
+
callbacks.closeAdmission();
|
|
268
|
+
disposal ??= miniflare.dispose();
|
|
269
|
+
return disposal;
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
async function awaitCleanup(disposal, callbacks) {
|
|
273
|
+
const [disposed] = await Promise.allSettled([disposal, callbacks.drain()]);
|
|
274
|
+
if (disposed.status === "rejected")
|
|
275
|
+
throw disposed.reason;
|
|
276
|
+
}
|
|
277
|
+
async function executeInvocation(request, signal) {
|
|
278
|
+
signal.throwIfAborted();
|
|
279
|
+
let bundle;
|
|
280
|
+
try {
|
|
281
|
+
bundle = await workerBundle;
|
|
282
|
+
}
|
|
283
|
+
catch (rawCause) {
|
|
284
|
+
throw new SandboxCrashed({ rawCause });
|
|
285
|
+
}
|
|
286
|
+
signal.throwIfAborted();
|
|
287
|
+
const callbacks = createInvocationCallbacks(request);
|
|
288
|
+
let miniflare;
|
|
289
|
+
try {
|
|
290
|
+
miniflare = createInvocationMiniflare(bundle, callbacks.fetch);
|
|
291
|
+
}
|
|
292
|
+
catch (rawCause) {
|
|
293
|
+
throw new SandboxCrashed({ rawCause });
|
|
294
|
+
}
|
|
295
|
+
const beginDisposal = createDisposal(miniflare, callbacks);
|
|
296
|
+
const onAbort = () => {
|
|
297
|
+
void beginDisposal().catch((cause) => {
|
|
298
|
+
console.error("Local workerd disposal failed", cause);
|
|
299
|
+
return undefined;
|
|
300
|
+
});
|
|
301
|
+
};
|
|
302
|
+
if (signal.aborted)
|
|
303
|
+
onAbort();
|
|
304
|
+
else
|
|
305
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
306
|
+
let result;
|
|
307
|
+
try {
|
|
308
|
+
signal.throwIfAborted();
|
|
309
|
+
result = await dispatchInvocation(miniflare, request, signal);
|
|
310
|
+
}
|
|
311
|
+
catch (cause) {
|
|
312
|
+
result = { ok: false, cause: sandboxFailure(cause) };
|
|
313
|
+
}
|
|
314
|
+
finally {
|
|
315
|
+
signal.removeEventListener("abort", onAbort);
|
|
316
|
+
}
|
|
317
|
+
try {
|
|
318
|
+
await awaitCleanup(beginDisposal(), callbacks);
|
|
319
|
+
}
|
|
320
|
+
catch (cause) {
|
|
321
|
+
// Disposal is an operator annotation. Keep the original guest/transport outcome.
|
|
322
|
+
console.error("sandbox_cleanup_failure", {
|
|
323
|
+
executionId: request.executionId,
|
|
324
|
+
rawCause: cause,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
if (!result.ok) {
|
|
328
|
+
throw recoverSandboxFailure(result.cause, callbacks.failures);
|
|
329
|
+
}
|
|
330
|
+
return result.value;
|
|
331
|
+
}
|
|
332
|
+
/** Construct the private per-invocation workerd Sandbox. */
|
|
333
|
+
export function createPrivateWorkerdSandbox() {
|
|
334
|
+
return Object.freeze({
|
|
335
|
+
adapter: "local-workerd",
|
|
336
|
+
execute: async (request, signal) => {
|
|
337
|
+
try {
|
|
338
|
+
return await executeInvocation(request, signal);
|
|
339
|
+
}
|
|
340
|
+
catch (cause) {
|
|
341
|
+
throw signal.aborted
|
|
342
|
+
? new Cancelled({ by: "caller" })
|
|
343
|
+
: sandboxFailure(cause);
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type ShiitakeSession } from "@fungi.computer/shiitake/client";
|
|
2
|
+
import type { FetchLike } from "@modelcontextprotocol/client";
|
|
3
|
+
import { type AppDirectory } from "@fungi.computer/oyster/shell/app-directory";
|
|
4
|
+
import { type GillRequest, type GillReply } from "@fungi.computer/gills";
|
|
5
|
+
import { type FungiAccountTeam } from "./private-fungi-account.js";
|
|
6
|
+
/** A selected Team's App reads. Credentials and selectors remain Local-owned. */
|
|
7
|
+
export type LocalTeamApps = Readonly<{
|
|
8
|
+
team: FungiAccountTeam;
|
|
9
|
+
openSession: (target: Readonly<{
|
|
10
|
+
agentId: string;
|
|
11
|
+
sessionId: string;
|
|
12
|
+
}>, signal: AbortSignal) => Promise<Readonly<{
|
|
13
|
+
session: ShiitakeSession;
|
|
14
|
+
close: () => Promise<void>;
|
|
15
|
+
}>>;
|
|
16
|
+
surfaceUrl: (surfaceId: string, signal?: AbortSignal) => Promise<URL>;
|
|
17
|
+
read: (signal?: AbortSignal) => Promise<AppDirectory>;
|
|
18
|
+
requestGill: (surfaceId: string, selectors: GillRequest, signal?: AbortSignal) => Promise<GillReply>;
|
|
19
|
+
}>;
|
|
20
|
+
/** Pin this view to the saved account and Team; a later switch retires it. */
|
|
21
|
+
export declare function openLocalTeamApps(options?: Readonly<{
|
|
22
|
+
credentialPath?: string;
|
|
23
|
+
fetch?: FetchLike;
|
|
24
|
+
}>): Promise<LocalTeamApps>;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { decodeGillRefusal } from "@fungi.computer/agar/gills";
|
|
2
|
+
import { EventSource } from "eventsource";
|
|
3
|
+
import { createShiitakeClient, } from "@fungi.computer/shiitake/client";
|
|
4
|
+
import { appDirectoryResponseSchema, } from "@fungi.computer/oyster/shell/app-directory";
|
|
5
|
+
import { createGillClient, } from "@fungi.computer/gills";
|
|
6
|
+
import { fungiAccountFetch, readFungiAccountStatus, } from "./private-fungi-account.js";
|
|
7
|
+
/** Pin this view to the saved account and Team; a later switch retires it. */
|
|
8
|
+
export async function openLocalTeamApps(options = {}) {
|
|
9
|
+
const account = await readFungiAccountStatus(options);
|
|
10
|
+
if (account?.team === undefined)
|
|
11
|
+
throw new Error("Sign in to Fungi and choose a Team to view its Apps");
|
|
12
|
+
const team = account.team;
|
|
13
|
+
const selectedIssuer = account.issuer;
|
|
14
|
+
const issuer = new URL(account.issuer).origin;
|
|
15
|
+
const prefix = `/api/teams/${encodeURIComponent(team.teamId)}`;
|
|
16
|
+
const transport = fungiAccountFetch(issuer, options);
|
|
17
|
+
async function stillSelected() {
|
|
18
|
+
const current = await readFungiAccountStatus(options);
|
|
19
|
+
if (current?.issuer !== selectedIssuer ||
|
|
20
|
+
current.team?.teamId !== team.teamId)
|
|
21
|
+
throw new Error("The selected Fungi Team changed; reopen the App pane");
|
|
22
|
+
}
|
|
23
|
+
async function read(signal) {
|
|
24
|
+
await stillSelected();
|
|
25
|
+
const response = await transport(`${issuer}${prefix}/local/apps`, {
|
|
26
|
+
signal: signal ?? null,
|
|
27
|
+
});
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
await response.body?.cancel();
|
|
30
|
+
throw new Error(`App directory refused with HTTP ${String(response.status)}`);
|
|
31
|
+
}
|
|
32
|
+
const raw = await response.json();
|
|
33
|
+
await stillSelected();
|
|
34
|
+
signal?.throwIfAborted();
|
|
35
|
+
return appDirectoryResponseSchema.parse(raw);
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
team,
|
|
39
|
+
read,
|
|
40
|
+
async surfaceUrl(surfaceId, signal) {
|
|
41
|
+
const directory = await read(signal);
|
|
42
|
+
const app = directory.apps.find((item) => item.surfaceId === surfaceId);
|
|
43
|
+
if (app?.availability !== "available" ||
|
|
44
|
+
app.surface === null ||
|
|
45
|
+
app.releaseId === null)
|
|
46
|
+
throw new Error("That App is not available in the selected Team");
|
|
47
|
+
const url = new URL(app.surfacePath, issuer);
|
|
48
|
+
if (url.origin !== issuer)
|
|
49
|
+
throw new Error("The App surface is not on the selected Hub");
|
|
50
|
+
url.searchParams.set("authorityVersion", String(app.authorityVersion));
|
|
51
|
+
url.searchParams.set("releaseId", app.releaseId);
|
|
52
|
+
url.searchParams.set("surfaceRoute", app.surface.stableRoute);
|
|
53
|
+
await stillSelected();
|
|
54
|
+
return url;
|
|
55
|
+
},
|
|
56
|
+
async openSession(target, signal) {
|
|
57
|
+
await stillSelected();
|
|
58
|
+
signal.throwIfAborted();
|
|
59
|
+
const fetch = Object.assign(async (input, init) => {
|
|
60
|
+
await stillSelected();
|
|
61
|
+
signal.throwIfAborted();
|
|
62
|
+
return transport(input, {
|
|
63
|
+
...init,
|
|
64
|
+
signal: AbortSignal.any([
|
|
65
|
+
signal,
|
|
66
|
+
...(init?.signal ? [init.signal] : []),
|
|
67
|
+
]),
|
|
68
|
+
});
|
|
69
|
+
}, { preconnect: globalThis.fetch.preconnect });
|
|
70
|
+
class AccountEvents extends EventSource {
|
|
71
|
+
constructor(url) {
|
|
72
|
+
super(url, { fetch });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const agent = createShiitakeClient({
|
|
76
|
+
baseUrl: `${issuer}${prefix}/local/agents/${encodeURIComponent(target.agentId)}`,
|
|
77
|
+
fetch,
|
|
78
|
+
eventSource: AccountEvents,
|
|
79
|
+
});
|
|
80
|
+
try {
|
|
81
|
+
const session = agent.session(target.sessionId);
|
|
82
|
+
await session.read();
|
|
83
|
+
await stillSelected();
|
|
84
|
+
signal.throwIfAborted();
|
|
85
|
+
return { session, close: agent.close };
|
|
86
|
+
}
|
|
87
|
+
catch (cause) {
|
|
88
|
+
await agent.close();
|
|
89
|
+
throw new Error("The selected Team Session could not be opened", {
|
|
90
|
+
cause,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
async requestGill(surfaceId, selectors, signal) {
|
|
95
|
+
const directory = await read(signal);
|
|
96
|
+
const app = directory.apps.find((item) => item.surfaceId === surfaceId);
|
|
97
|
+
const declaration = app?.gills.find((item) => item.id === selectors.viewId);
|
|
98
|
+
if (app === undefined ||
|
|
99
|
+
declaration === undefined ||
|
|
100
|
+
app.availability !== "available" ||
|
|
101
|
+
app.releaseId === null)
|
|
102
|
+
throw new Error("That Gill is not available in the selected Team");
|
|
103
|
+
const address = app.address.kind === "default"
|
|
104
|
+
? `default/${encodeURIComponent(app.address.appId)}`
|
|
105
|
+
: encodeURIComponent(app.address.installationId);
|
|
106
|
+
const endpoint = new URL(`${prefix}/apps/${address}/gills`, issuer);
|
|
107
|
+
endpoint.searchParams.set("authorityVersion", String(app.authorityVersion));
|
|
108
|
+
endpoint.searchParams.set("releaseId", app.releaseId);
|
|
109
|
+
if (app.surface !== null)
|
|
110
|
+
endpoint.searchParams.set("surfaceRoute", app.surface.stableRoute);
|
|
111
|
+
await stillSelected();
|
|
112
|
+
const response = await createGillClient({
|
|
113
|
+
decodeRefusal: decodeGillRefusal,
|
|
114
|
+
endpoint: endpoint.href,
|
|
115
|
+
fetch: transport,
|
|
116
|
+
declaration,
|
|
117
|
+
}).request(selectors, signal);
|
|
118
|
+
await stillSelected();
|
|
119
|
+
signal?.throwIfAborted();
|
|
120
|
+
return response;
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { freshFungiAccess } from "./private-fungi-account.js";
|
|
2
|
+
/** Git's get/store/erase protocol. Only the existing Botanical login is stored;
|
|
3
|
+
* a minted capability lives in this call and Git's credential response. */
|
|
4
|
+
export declare function runTrackerGitCredential(input: Readonly<{
|
|
5
|
+
action: string;
|
|
6
|
+
hub: string;
|
|
7
|
+
teamId: string;
|
|
8
|
+
stdin: AsyncIterable<Uint8Array>;
|
|
9
|
+
stdout: {
|
|
10
|
+
write(value: string): void;
|
|
11
|
+
};
|
|
12
|
+
}>, owners?: {
|
|
13
|
+
access?: typeof freshFungiAccess;
|
|
14
|
+
fetch?: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
15
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { parse } from "node:querystring";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { freshFungiAccess } from "./private-fungi-account.js";
|
|
4
|
+
const fields = z.object({
|
|
5
|
+
protocol: z.literal("https"),
|
|
6
|
+
host: z.string().min(1),
|
|
7
|
+
path: z.string().min(1),
|
|
8
|
+
});
|
|
9
|
+
const lease = z.strictObject({
|
|
10
|
+
remote: z.url({ protocol: /^https$/ }),
|
|
11
|
+
capability: z.string().regex(/^fgit_[0-9a-f]{64}$/u),
|
|
12
|
+
expiresAt: z.number().int().positive(),
|
|
13
|
+
permission: z.enum(["read", "write"]),
|
|
14
|
+
});
|
|
15
|
+
async function readBounded(input) {
|
|
16
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
17
|
+
let text = "";
|
|
18
|
+
let bytes = 0;
|
|
19
|
+
for await (const chunk of input) {
|
|
20
|
+
bytes += chunk.byteLength;
|
|
21
|
+
if (bytes > 8192)
|
|
22
|
+
throw new Error("Credential input too large");
|
|
23
|
+
text += decoder.decode(chunk, { stream: true });
|
|
24
|
+
const end = text.indexOf("\n\n");
|
|
25
|
+
if (end !== -1)
|
|
26
|
+
return text.slice(0, end);
|
|
27
|
+
}
|
|
28
|
+
return text + decoder.decode();
|
|
29
|
+
}
|
|
30
|
+
/** Git's get/store/erase protocol. Only the existing Botanical login is stored;
|
|
31
|
+
* a minted capability lives in this call and Git's credential response. */
|
|
32
|
+
export async function runTrackerGitCredential(input, owners = {}) {
|
|
33
|
+
try {
|
|
34
|
+
if (!["get", "store", "erase", "remote"].includes(input.action))
|
|
35
|
+
throw new Error("Invalid action");
|
|
36
|
+
// Store and erase are intentionally silent: this helper has no Git cache.
|
|
37
|
+
if (input.action === "store" || input.action === "erase")
|
|
38
|
+
return;
|
|
39
|
+
const values = input.action === "remote"
|
|
40
|
+
? null
|
|
41
|
+
: fields.parse(parse(await readBounded(input.stdin), "\n", "=", {
|
|
42
|
+
maxKeys: 32,
|
|
43
|
+
decodeURIComponent: (value) => value,
|
|
44
|
+
}));
|
|
45
|
+
const hub = new URL(input.hub);
|
|
46
|
+
if (hub.protocol !== "https:" ||
|
|
47
|
+
hub.username ||
|
|
48
|
+
hub.password ||
|
|
49
|
+
hub.search ||
|
|
50
|
+
hub.hash ||
|
|
51
|
+
hub.pathname !== "/")
|
|
52
|
+
throw new Error("Invalid Hub origin");
|
|
53
|
+
const teamId = z.string().min(1).max(256).parse(input.teamId);
|
|
54
|
+
const signal = AbortSignal.timeout(30_000);
|
|
55
|
+
const access = await (owners.access ?? freshFungiAccess)(hub, { signal });
|
|
56
|
+
const response = await (owners.fetch ?? fetch)(new URL(`/api/teams/${encodeURIComponent(teamId)}/git/tracker/credential`, hub), {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: { Authorization: `Bearer ${access}` },
|
|
59
|
+
redirect: "error",
|
|
60
|
+
signal,
|
|
61
|
+
});
|
|
62
|
+
if (!response.ok || response.body === null)
|
|
63
|
+
throw new Error("Credential exchange refused");
|
|
64
|
+
const granted = lease.parse(JSON.parse(await readBounded(response.body)));
|
|
65
|
+
const remote = new URL(granted.remote);
|
|
66
|
+
if (remote.username ||
|
|
67
|
+
remote.password ||
|
|
68
|
+
remote.search ||
|
|
69
|
+
remote.hash ||
|
|
70
|
+
granted.expiresAt <= Date.now())
|
|
71
|
+
return;
|
|
72
|
+
if (values === null) {
|
|
73
|
+
// Discover the configured remote through the same admitted exchange;
|
|
74
|
+
// the unused five-minute capability is never emitted or cached.
|
|
75
|
+
input.stdout.write(`${remote.href}\n`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (remote.host !== values.host || remote.pathname !== `/${values.path}`)
|
|
79
|
+
return;
|
|
80
|
+
input.stdout.write(`username=fungi\npassword=${granted.capability}\npassword_expiry_utc=${String(Math.floor(granted.expiresAt / 1000))}\n\n`);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Neither schema-decode details, OAuth failures nor transport text may
|
|
84
|
+
// expose supplied credentials through the CLI's top-level error boundary.
|
|
85
|
+
throw new Error("Tracker Git unavailable. Check the remote, Team consent and Fungi login with git:tracker access.");
|
|
86
|
+
}
|
|
87
|
+
}
|