@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.
- package/README.md +282 -34
- package/dist/auth-cli.d.ts +10 -0
- package/dist/auth-cli.js +36 -14
- package/dist/fungi-account-command.d.ts +1 -1
- package/dist/fungi-account-command.js +6 -3
- package/dist/index.d.ts +10 -2
- package/dist/index.js +8 -2
- package/dist/internal/image-fit-metafile.json +106 -0
- package/dist/internal/local-image-fit-worker.mjs +960 -0
- package/dist/internal/private-workerd-sandbox-worker.mjs +11 -12
- package/dist/internal/sandbox-metafile.json +7 -7
- package/dist/local-archive-handoff.d.ts +12 -0
- package/dist/local-archive-handoff.js +21 -0
- package/dist/local-extensions.d.ts +58 -12
- package/dist/local-extensions.js +195 -55
- package/dist/local-image-fit.d.ts +30 -0
- package/dist/local-image-fit.js +105 -0
- package/dist/local-models.d.ts +4 -2
- package/dist/local-models.js +11 -6
- package/dist/local-session-archives.d.ts +8 -0
- package/dist/local-session-archives.js +152 -0
- package/dist/local-skill-source.d.ts +18 -3
- package/dist/local-skill-source.js +216 -140
- package/dist/log-event.d.ts +7 -0
- package/dist/log-event.js +8 -4
- package/dist/private-bounded-response.d.ts +2 -0
- package/dist/private-bounded-response.js +41 -0
- package/dist/private-file-credential-store.d.ts +28 -3
- package/dist/private-file-credential-store.js +57 -9
- package/dist/private-files-patch.d.ts +2 -1
- package/dist/private-files-patch.js +21 -3
- package/dist/private-fungi-account.d.ts +40 -23
- package/dist/private-fungi-account.js +201 -21
- package/dist/private-fungi-mcp-connection.d.ts +4 -0
- package/dist/private-fungi-mcp-connection.js +25 -6
- package/dist/private-fungi-mcp-selection-schema.d.ts +7 -0
- package/dist/private-fungi-mcp-selection-schema.js +15 -0
- package/dist/private-fungi-mcp-selection.d.ts +6 -6
- package/dist/private-fungi-mcp-selection.js +29 -16
- package/dist/private-fungi-model-api.js +4 -4
- package/dist/private-fungi-model-catalog.d.ts +10 -0
- package/dist/private-fungi-model-catalog.js +10 -2
- package/dist/private-fungi-model-provider.d.ts +1 -1
- package/dist/private-fungi-model-provider.js +6 -1
- package/dist/private-fungi-pending-logouts.d.ts +1 -1
- package/dist/private-fungi-pending-logouts.js +1 -2
- package/dist/private-fungi-service-logout.d.ts +44 -0
- package/dist/private-fungi-service-logout.js +59 -0
- package/dist/private-invocation-computer.d.ts +24 -3
- package/dist/private-invocation-computer.js +101 -62
- package/dist/private-invocation-files.d.ts +16 -1
- package/dist/private-invocation-files.js +154 -83
- package/dist/private-local-acp-stdio.js +3 -0
- package/dist/private-local-admission-gate.d.ts +14 -0
- package/dist/private-local-admission-gate.js +52 -0
- package/dist/private-local-agent-control.d.ts +101 -0
- package/dist/private-local-agent-control.js +30 -0
- package/dist/private-local-agent-default-model.d.ts +12 -0
- package/dist/private-local-agent-default-model.js +31 -0
- package/dist/private-local-auth-rpc.d.ts +67 -7
- package/dist/private-local-auth-rpc.js +140 -17
- package/dist/private-local-bwrap.d.ts +13 -0
- package/dist/private-local-bwrap.js +70 -0
- package/dist/private-local-client-version.d.ts +5 -0
- package/dist/private-local-client-version.js +43 -0
- package/dist/private-local-computer-attach.d.ts +3 -3
- package/dist/private-local-computer-attach.js +17 -3
- package/dist/private-local-computer-processes.d.ts +6 -3
- package/dist/private-local-computer-processes.js +55 -31
- package/dist/private-local-config-root.d.ts +21 -0
- package/dist/private-local-config-root.js +19 -0
- package/dist/private-local-containment.d.ts +51 -0
- package/dist/private-local-containment.js +161 -0
- package/dist/private-local-delegation.d.ts +10 -3
- package/dist/private-local-delegation.js +70 -15
- package/dist/private-local-effective-model.d.ts +9 -1
- package/dist/private-local-effective-model.js +3 -0
- package/dist/private-local-exec-environment.d.ts +16 -0
- package/dist/private-local-exec-environment.js +11 -0
- package/dist/private-local-linux-sandbox.d.ts +8 -0
- package/dist/private-local-linux-sandbox.js +67 -0
- package/dist/private-local-mail.d.ts +4 -2
- package/dist/private-local-mail.js +41 -33
- package/dist/private-local-mesh-enrollment.d.ts +34 -0
- package/dist/private-local-mesh-enrollment.js +188 -0
- package/dist/private-local-mesh-links.d.ts +21 -0
- package/dist/private-local-mesh-links.js +229 -0
- package/dist/private-local-mesh-record.d.ts +120 -0
- package/dist/private-local-mesh-record.js +55 -0
- package/dist/private-local-process-identity.d.ts +20 -0
- package/dist/private-local-process-identity.js +40 -0
- package/dist/private-local-runtime-host.d.ts +64 -13
- package/dist/private-local-runtime-host.js +361 -172
- package/dist/private-local-sandbox-policy.d.ts +21 -0
- package/dist/private-local-sandbox-policy.js +121 -0
- package/dist/private-local-sandbox.d.ts +37 -0
- package/dist/private-local-sandbox.js +114 -0
- package/dist/private-local-seatbelt.d.ts +24 -0
- package/dist/private-local-seatbelt.js +68 -0
- package/dist/private-local-service-contract.d.ts +91 -11
- package/dist/private-local-service-discovery.d.ts +129 -13
- package/dist/private-local-service-discovery.js +148 -47
- package/dist/private-local-service-lease.d.ts +16 -0
- package/dist/private-local-service-lease.js +105 -0
- package/dist/private-local-service-listener.d.ts +9 -2
- package/dist/private-local-service-listener.js +260 -64
- package/dist/private-local-service-log.d.ts +128 -0
- package/dist/private-local-service-log.js +321 -0
- package/dist/private-local-service-owner.d.ts +87 -1519
- package/dist/private-local-service-owner.js +1069 -71
- package/dist/private-local-service-process.js +3 -1
- package/dist/private-local-service-termination.d.ts +7 -0
- package/dist/private-local-service-termination.js +67 -0
- package/dist/private-local-service.d.ts +31 -2
- package/dist/private-local-service.js +402 -126
- package/dist/private-local-session-discovery.d.ts +52 -5
- package/dist/private-local-session-discovery.js +132 -11
- package/dist/private-local-shiitake-host.js +2 -2
- package/dist/private-local-startup-model.d.ts +51 -0
- package/dist/private-local-startup-model.js +86 -0
- package/dist/private-local-stop-all.d.ts +27 -0
- package/dist/private-local-stop-all.js +26 -0
- package/dist/private-local-ui-attachment.d.ts +19 -0
- package/dist/private-local-ui-attachment.js +71 -0
- package/dist/private-local-worker-server.d.ts +1 -0
- package/dist/private-local-worker-server.js +13 -5
- package/dist/private-mcp-form-contract.d.ts +186 -0
- package/dist/private-mcp-form-contract.js +53 -0
- package/dist/private-mcp-form-elicitation.d.ts +18 -0
- package/dist/private-mcp-form-elicitation.js +97 -0
- package/dist/private-mcp-form-session.d.ts +172 -0
- package/dist/private-mcp-form-session.js +148 -0
- package/dist/private-openai-claims.d.ts +30 -0
- package/dist/private-openai-claims.js +69 -0
- package/dist/private-oyster-mcp-auth.d.ts +13 -0
- package/dist/private-oyster-mcp-auth.js +57 -0
- package/dist/private-provider-rejection.d.ts +15 -0
- package/dist/private-provider-rejection.js +24 -0
- package/dist/private-sqlite-owner.d.ts +34 -2
- package/dist/private-sqlite-owner.js +103 -37
- package/dist/private-team-computers.d.ts +4 -4
- package/dist/private-team-computers.js +11 -2
- package/dist/seatbelt/safe-base.sbpl +79 -0
- package/dist/subscription-activation.d.ts +9 -0
- package/dist/subscription-activation.js +62 -0
- package/dist/subscription-identity.d.ts +23 -0
- package/dist/subscription-identity.js +124 -0
- package/dist/subscription-turn.d.ts +17 -0
- package/dist/subscription-turn.js +143 -0
- package/dist/team-apps.d.ts +2 -2
- package/dist/team-apps.js +7 -2
- package/native/bin/LICENSE.bubblewrap +502 -0
- package/native/bin/LICENSE.libcap +112 -0
- package/native/bin/LICENSE.sandbox-runtime +201 -0
- package/native/bin/SOURCE.bubblewrap.tar.xz +0 -0
- package/native/bin/bwrap-build-source/Dockerfile +48 -0
- package/native/bin/bwrap-build-source/README.md +96 -0
- package/native/bin/bwrap-build-source/build.mjs +52 -0
- package/native/bin/bwrap-build-source/linux-arm64.ini +14 -0
- package/native/bin/bwrap-build-source/pin.mjs +21 -0
- package/native/bin/bwrap-build-source/record.mjs +74 -0
- package/native/bin/bwrap-build-source/vendor/LICENSE.sandbox-runtime +201 -0
- package/native/bin/bwrap-build-source/vendor/README.md +23 -0
- package/native/bin/bwrap-build-source/vendor/seccomp-unix-block.c +280 -0
- package/native/bin/bwrap-build.json +44 -0
- package/native/bin/fungi-bwrap +0 -0
- package/native/bin/fungi-bwrap.sha256 +1 -0
- package/native/bin/fungi-seccomp.bpf +0 -0
- package/native/bin/fungi-seccomp.bpf.sha256 +1 -0
- package/package.json +44 -31
- package/dist/private-local-model-settings.d.ts +0 -5
- package/dist/private-local-model-settings.js +0 -31
- package/dist/private-local-subagents.d.ts +0 -27
- package/dist/private-local-subagents.js +0 -77
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { correlationKey } from "@fungi.computer/cairn";
|
|
2
|
+
import { defineExtension } from "@fungi.computer/shiitake/extensions";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import { mcpFormApprovalContract, } from "./private-mcp-form-contract.js";
|
|
5
|
+
import { consumeLocalUiCommand, runLocalUiCommand, startLocalUiWatch, } from "./private-local-ui-attachment.js";
|
|
6
|
+
const maximumPendingForms = 100;
|
|
7
|
+
/** Host-owned references to canonical approvals; no suspended tool or second ledger. */
|
|
8
|
+
export function createMcpFormSession() {
|
|
9
|
+
const pending = new Map();
|
|
10
|
+
let publish;
|
|
11
|
+
const extension = defineExtension({
|
|
12
|
+
id: mcpFormApprovalContract.id,
|
|
13
|
+
version: "1.0.0",
|
|
14
|
+
}).define({
|
|
15
|
+
commands: {
|
|
16
|
+
specs: mcpFormApprovalContract.commands,
|
|
17
|
+
handlers: (context) => ({
|
|
18
|
+
answer: ({ sessionId, input }) => Effect.gen(function* () {
|
|
19
|
+
const onComplete = consumeLocalUiCommand(sessionId);
|
|
20
|
+
if (onComplete === undefined) {
|
|
21
|
+
const result = {
|
|
22
|
+
kind: "refused",
|
|
23
|
+
reason: "not-attached",
|
|
24
|
+
};
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
const entry = pending.get(input.id);
|
|
28
|
+
if (entry?.sessionId !== sessionId) {
|
|
29
|
+
const result = {
|
|
30
|
+
kind: "refused",
|
|
31
|
+
reason: "not-pending",
|
|
32
|
+
};
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
if (entry.answering) {
|
|
36
|
+
const result = {
|
|
37
|
+
kind: "refused",
|
|
38
|
+
reason: "in-flight",
|
|
39
|
+
};
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
entry.answering = true;
|
|
43
|
+
onComplete(async (committed) => {
|
|
44
|
+
try {
|
|
45
|
+
if (!committed)
|
|
46
|
+
return { kind: "refused", reason: "not-attached" };
|
|
47
|
+
// Session commit and live UI recheck precede the canonical
|
|
48
|
+
// decision. Its bound host, never MCP accept, owns apply.
|
|
49
|
+
return await entry.approvals.answer({
|
|
50
|
+
recordId: entry.approval.record.recordId,
|
|
51
|
+
decision: input.decision,
|
|
52
|
+
expectedRevision: input.expectedRevision,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
entry.answering = false;
|
|
57
|
+
const view = await entry.approvals.get({
|
|
58
|
+
recordId: entry.approval.record.recordId,
|
|
59
|
+
});
|
|
60
|
+
if (view?.record.state !== "pending")
|
|
61
|
+
pending.delete(input.id);
|
|
62
|
+
else
|
|
63
|
+
entry.approval = view;
|
|
64
|
+
await publish?.(sessionId);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
yield* context.observe(sessionId);
|
|
68
|
+
const result = { kind: "submitted" };
|
|
69
|
+
return result;
|
|
70
|
+
}),
|
|
71
|
+
}),
|
|
72
|
+
},
|
|
73
|
+
hooks: {
|
|
74
|
+
start: (context) => {
|
|
75
|
+
publish = (sessionId) => Effect.runPromise(context.transact(context.observe(sessionId)));
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
projection: {
|
|
79
|
+
schema: mcpFormApprovalContract.projection.schema,
|
|
80
|
+
read: (_context, sessionId) => ({
|
|
81
|
+
pending: [...pending.entries()]
|
|
82
|
+
.filter(([, entry]) => entry.sessionId === sessionId)
|
|
83
|
+
.map(([id, entry]) => ({ id, approval: entry.approval })),
|
|
84
|
+
}),
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
/**
|
|
88
|
+
* Native host port, never a model or UI command. The caller supplies the
|
|
89
|
+
* actual submit result and a member-bound capability, not form JSON.
|
|
90
|
+
*/
|
|
91
|
+
async function presentApproval(sessionId, submitted, approvals) {
|
|
92
|
+
const approval = await approvals.get({ recordId: submitted.recordId });
|
|
93
|
+
if (approval?.record.state !== "pending" ||
|
|
94
|
+
approval.record.requester.sessionId !== sessionId)
|
|
95
|
+
return { kind: "refused", reason: "not-pending" };
|
|
96
|
+
const existing = [...pending.entries()].find(([, entry]) => entry.sessionId === sessionId &&
|
|
97
|
+
entry.approval.record.recordId === submitted.recordId);
|
|
98
|
+
if (existing !== undefined) {
|
|
99
|
+
const [id, entry] = existing;
|
|
100
|
+
entry.approval = approval;
|
|
101
|
+
entry.approvals = approvals;
|
|
102
|
+
await publish?.(sessionId);
|
|
103
|
+
return { kind: "presented", id };
|
|
104
|
+
}
|
|
105
|
+
if (pending.size >= maximumPendingForms)
|
|
106
|
+
return { kind: "refused", reason: "capacity" };
|
|
107
|
+
const id = correlationKey();
|
|
108
|
+
pending.set(id, { sessionId, approval, approvals, answering: false });
|
|
109
|
+
await publish?.(sessionId);
|
|
110
|
+
return { kind: "presented", id };
|
|
111
|
+
}
|
|
112
|
+
function bindSession(sessionId, session, attachment) {
|
|
113
|
+
if (attachment === undefined)
|
|
114
|
+
return session;
|
|
115
|
+
return {
|
|
116
|
+
...session,
|
|
117
|
+
extension: (id) => {
|
|
118
|
+
const client = session.extension(id);
|
|
119
|
+
return {
|
|
120
|
+
...client,
|
|
121
|
+
call: (command, input) => id === mcpFormApprovalContract.id && command === "answer"
|
|
122
|
+
? runLocalUiCommand(attachment, sessionId, () => client.call(command, input))
|
|
123
|
+
: client.call(command, input),
|
|
124
|
+
};
|
|
125
|
+
},
|
|
126
|
+
watch: async (input) => {
|
|
127
|
+
const stopWatching = startLocalUiWatch(attachment, sessionId);
|
|
128
|
+
if (stopWatching === undefined)
|
|
129
|
+
return session.watch(input);
|
|
130
|
+
try {
|
|
131
|
+
const watch = await session.watch(input);
|
|
132
|
+
return {
|
|
133
|
+
...watch,
|
|
134
|
+
close: async () => {
|
|
135
|
+
stopWatching();
|
|
136
|
+
await watch.close();
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
stopWatching();
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return { extension, presentApproval, bindSession };
|
|
148
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { decodeJwt } from "jose";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
/**
|
|
4
|
+
* Why a sign-in proved no person: the token or claim at fault and the claim
|
|
5
|
+
* NAMES present. Never claim values, emails, subjects or tokens.
|
|
6
|
+
*/
|
|
7
|
+
export declare const openAIIdentityDetailSchema: z.ZodObject<{
|
|
8
|
+
claim: z.ZodEnum<{
|
|
9
|
+
access_token: "access_token";
|
|
10
|
+
"access_token.iss": "access_token.iss";
|
|
11
|
+
"access_token.sub": "access_token.sub";
|
|
12
|
+
}>;
|
|
13
|
+
problem: z.ZodEnum<{
|
|
14
|
+
absent: "absent";
|
|
15
|
+
malformed: "malformed";
|
|
16
|
+
invalid: "invalid";
|
|
17
|
+
}>;
|
|
18
|
+
claimNames: z.ZodArray<z.ZodString>;
|
|
19
|
+
authClaimNames: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
20
|
+
}, z.core.$strict>;
|
|
21
|
+
export type OpenAIIdentityDetail = z.output<typeof openAIIdentityDetailSchema>;
|
|
22
|
+
/** The person a direct-access token belongs to: its issuer and subject. */
|
|
23
|
+
export declare function readOpenAIPerson(access: string, decode?: typeof decodeJwt): Readonly<{
|
|
24
|
+
kind: "person";
|
|
25
|
+
iss: string;
|
|
26
|
+
sub: string;
|
|
27
|
+
}> | Readonly<{
|
|
28
|
+
kind: "unproven";
|
|
29
|
+
detail: OpenAIIdentityDetail;
|
|
30
|
+
}>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { decodeJwt, errors } from "jose";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
const identityComponent = z
|
|
4
|
+
.string()
|
|
5
|
+
.min(1)
|
|
6
|
+
.max(256)
|
|
7
|
+
.refine((value) => value.trim().length > 0);
|
|
8
|
+
const authNamespace = "https://api.openai.com/auth";
|
|
9
|
+
const claimObjectSchema = z.record(z.string(), z.json());
|
|
10
|
+
/** At most this many claim names, each at most this long, ever cross. */
|
|
11
|
+
const claimNameLimit = 32;
|
|
12
|
+
const claimNameLength = 64;
|
|
13
|
+
const claimNamesSchema = z
|
|
14
|
+
.array(z.string().max(claimNameLength))
|
|
15
|
+
.max(claimNameLimit);
|
|
16
|
+
/**
|
|
17
|
+
* Why a sign-in proved no person: the token or claim at fault and the claim
|
|
18
|
+
* NAMES present. Never claim values, emails, subjects or tokens.
|
|
19
|
+
*/
|
|
20
|
+
export const openAIIdentityDetailSchema = z.strictObject({
|
|
21
|
+
claim: z.enum(["access_token", "access_token.iss", "access_token.sub"]),
|
|
22
|
+
problem: z.enum(["malformed", "absent", "invalid"]),
|
|
23
|
+
claimNames: claimNamesSchema,
|
|
24
|
+
authClaimNames: claimNamesSchema.optional(),
|
|
25
|
+
});
|
|
26
|
+
const names = (claims) => Object.keys(claims)
|
|
27
|
+
.slice(0, claimNameLimit)
|
|
28
|
+
.map((name) => name.slice(0, claimNameLength));
|
|
29
|
+
/** Decode JWT claims; only jose's JWTInvalid means a malformed token. */
|
|
30
|
+
function claimsOf(token, decode) {
|
|
31
|
+
try {
|
|
32
|
+
return claimObjectSchema.parse(decode(token));
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
// Anything other than JWTInvalid is a real fault, not a malformed token.
|
|
36
|
+
if (error instanceof errors.JWTInvalid)
|
|
37
|
+
return undefined;
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** The person a direct-access token belongs to: its issuer and subject. */
|
|
42
|
+
export function readOpenAIPerson(access, decode = decodeJwt) {
|
|
43
|
+
const claims = claimsOf(access, decode);
|
|
44
|
+
if (claims === undefined)
|
|
45
|
+
return {
|
|
46
|
+
kind: "unproven",
|
|
47
|
+
detail: { claim: "access_token", problem: "malformed", claimNames: [] },
|
|
48
|
+
};
|
|
49
|
+
const claimNames = names(claims);
|
|
50
|
+
const auth = claimObjectSchema.safeParse(claims[authNamespace]);
|
|
51
|
+
const context = auth.success
|
|
52
|
+
? { claimNames, authClaimNames: names(auth.data) }
|
|
53
|
+
: { claimNames };
|
|
54
|
+
const unproven = (claim, key) => ({
|
|
55
|
+
kind: "unproven",
|
|
56
|
+
detail: {
|
|
57
|
+
claim,
|
|
58
|
+
problem: key in claims ? "invalid" : "absent",
|
|
59
|
+
...context,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
const iss = identityComponent.safeParse(claims.iss);
|
|
63
|
+
if (!iss.success)
|
|
64
|
+
return unproven("access_token.iss", "iss");
|
|
65
|
+
const sub = identityComponent.safeParse(claims.sub);
|
|
66
|
+
if (!sub.success)
|
|
67
|
+
return unproven("access_token.sub", "sub");
|
|
68
|
+
return { kind: "person", iss: iss.data, sub: sub.data };
|
|
69
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { UnauthorizedError, type AuthProvider } from "@modelcontextprotocol/client";
|
|
2
|
+
/** A selected Oyster refused its key; never fall through to Hub OAuth. */
|
|
3
|
+
export declare class OysterMcpKeyRejectedError extends UnauthorizedError {
|
|
4
|
+
constructor();
|
|
5
|
+
}
|
|
6
|
+
/** Read bounded UTF-8 stdin, then place the key in Local's existing private store. */
|
|
7
|
+
export declare function saveOysterMcpKey(endpoint: URL, input: AsyncIterable<Uint8Array>, options?: {
|
|
8
|
+
credentialPath?: string;
|
|
9
|
+
}): Promise<void>;
|
|
10
|
+
/** Static bearer auth reads only the selected origin's credential, without discovery. */
|
|
11
|
+
export declare function oysterMcpKeyAuth(endpoint: URL, options?: {
|
|
12
|
+
credentialPath?: string;
|
|
13
|
+
}): AuthProvider;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { UnauthorizedError, } from "@modelcontextprotocol/client";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { createPrivateCredentialStore, maximumCredentialStringLength, } from "./private-file-credential-store.js";
|
|
4
|
+
import { parseFungiMcpEndpoint } from "./private-fungi-mcp-endpoint.js";
|
|
5
|
+
import { capturePrivateLocalConfigRoot } from "./private-local-config-root.js";
|
|
6
|
+
/** A selected Oyster refused its key; never fall through to Hub OAuth. */
|
|
7
|
+
export class OysterMcpKeyRejectedError extends UnauthorizedError {
|
|
8
|
+
constructor() {
|
|
9
|
+
super("The Oyster MCP key was refused. Replace it with fungi mcp use <url> --from-file -.");
|
|
10
|
+
this.name = "OysterMcpKeyRejectedError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function credentialId(endpoint) {
|
|
14
|
+
return `oyster-mcp:${parseFungiMcpEndpoint(endpoint).origin}`;
|
|
15
|
+
}
|
|
16
|
+
// Ear Tag's Oyster producer uses the oyk family and nanoid's URL-safe alphabet.
|
|
17
|
+
const oysterKey = z
|
|
18
|
+
.string()
|
|
19
|
+
.trim()
|
|
20
|
+
.min(5)
|
|
21
|
+
.max(maximumCredentialStringLength)
|
|
22
|
+
.regex(/^oyk_[A-Za-z0-9_-]+$/);
|
|
23
|
+
/** Read bounded UTF-8 stdin, then place the key in Local's existing private store. */
|
|
24
|
+
export async function saveOysterMcpKey(endpoint, input, options = {}) {
|
|
25
|
+
const id = credentialId(endpoint);
|
|
26
|
+
const store = createPrivateCredentialStore(options.credentialPath ?? capturePrivateLocalConfigRoot().credentialPath);
|
|
27
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
28
|
+
let bytes = 0;
|
|
29
|
+
let text = "";
|
|
30
|
+
for await (const chunk of input) {
|
|
31
|
+
bytes += chunk.byteLength;
|
|
32
|
+
if (bytes > maximumCredentialStringLength)
|
|
33
|
+
throw new Error("Oyster key input is too large");
|
|
34
|
+
text += decoder.decode(chunk, { stream: true });
|
|
35
|
+
}
|
|
36
|
+
text += decoder.decode();
|
|
37
|
+
const parsed = oysterKey.safeParse(text);
|
|
38
|
+
if (!parsed.success)
|
|
39
|
+
throw new Error("Expected one Oyster oyk_ key on stdin");
|
|
40
|
+
await store.modify(id, () => Promise.resolve({ type: "api_key", key: parsed.data }));
|
|
41
|
+
}
|
|
42
|
+
/** Static bearer auth reads only the selected origin's credential, without discovery. */
|
|
43
|
+
export function oysterMcpKeyAuth(endpoint, options = {}) {
|
|
44
|
+
const id = credentialId(endpoint);
|
|
45
|
+
const store = createPrivateCredentialStore(options.credentialPath ?? capturePrivateLocalConfigRoot().credentialPath);
|
|
46
|
+
return {
|
|
47
|
+
async token() {
|
|
48
|
+
const credential = await store.read(id);
|
|
49
|
+
if (credential?.type !== "api_key" || credential.key === undefined)
|
|
50
|
+
throw new OysterMcpKeyRejectedError();
|
|
51
|
+
return credential.key;
|
|
52
|
+
},
|
|
53
|
+
onUnauthorized() {
|
|
54
|
+
return Promise.reject(new OysterMcpKeyRejectedError());
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { ProviderRejection } from "@fungi.computer/mule";
|
|
3
|
+
/** Evidence owned by one Local request, never inferred from assistant prose. */
|
|
4
|
+
export type ModelFailureInput = Readonly<{
|
|
5
|
+
refusal?: Readonly<{
|
|
6
|
+
category: string;
|
|
7
|
+
message: string;
|
|
8
|
+
}> | undefined;
|
|
9
|
+
upstream?: AssistantMessage | undefined;
|
|
10
|
+
rejection?: ProviderRejection | undefined;
|
|
11
|
+
}>;
|
|
12
|
+
/** Local retries only a typed transient, never cancellation or a turn refusal. */
|
|
13
|
+
export declare function modelFailureRetryable(failure: ModelFailureInput): boolean;
|
|
14
|
+
/** The one owned wording for a failed Local model request. */
|
|
15
|
+
export declare function modelFailureMessage(provider: string, failure: ModelFailureInput): string;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { isContextOverflow } from "@earendil-works/pi-ai/utils/overflow";
|
|
2
|
+
/** Local retries only a typed transient, never cancellation or a turn refusal. */
|
|
3
|
+
export function modelFailureRetryable(failure) {
|
|
4
|
+
return (failure.refusal === undefined &&
|
|
5
|
+
failure.upstream?.stopReason !== "aborted" &&
|
|
6
|
+
failure.rejection?.code === "provider_unavailable");
|
|
7
|
+
}
|
|
8
|
+
/** The one owned wording for a failed Local model request. */
|
|
9
|
+
export function modelFailureMessage(provider, failure) {
|
|
10
|
+
if (failure.refusal !== undefined)
|
|
11
|
+
return `${failure.refusal.category}: ${failure.refusal.message}`;
|
|
12
|
+
// Typed subscription evidence wins over all SDK/provider wording.
|
|
13
|
+
if (failure.rejection?.code === "usage-limit-reached")
|
|
14
|
+
return `${provider} usage limit reached (usage-limit-reached). Check ChatGPT Settings > Usage, then try again later.`;
|
|
15
|
+
if (modelFailureRetryable(failure))
|
|
16
|
+
return `${provider} service unavailable. Try again shortly.`;
|
|
17
|
+
if (failure.upstream !== undefined) {
|
|
18
|
+
if (isContextOverflow(failure.upstream))
|
|
19
|
+
return `${provider} request exceeds the context window. Compact the Session or choose another model.`;
|
|
20
|
+
}
|
|
21
|
+
if (failure.rejection?.code === "provider_rejected")
|
|
22
|
+
return `${provider} rejected the model request (${failure.rejection.code}, HTTP ${String(failure.rejection.status)}). Check /login or choose another model with /model.`;
|
|
23
|
+
return `${provider} rejected the model request. Check /login or choose another model with /model.`;
|
|
24
|
+
}
|
|
@@ -1,7 +1,39 @@
|
|
|
1
1
|
import { DatabaseSync } from "node:sqlite";
|
|
2
2
|
import type { SqliteOwner } from "@fungi.computer/shiitake";
|
|
3
|
+
export type NativeOutput = null | number | bigint | string | Uint8Array;
|
|
4
|
+
export type NativeInput = null | number | string | Uint8Array;
|
|
5
|
+
/** Bun keeps its native transaction and deterministic statement-close APIs. */
|
|
6
|
+
export type PrivateBunSqliteDatabase = {
|
|
7
|
+
readonly isTransaction: boolean;
|
|
8
|
+
exec: (statement: string) => void;
|
|
9
|
+
prepare: (statement: string) => {
|
|
10
|
+
all: (...bindings: NativeInput[]) => Record<string, NativeOutput>[];
|
|
11
|
+
get: (...bindings: NativeInput[]) => Record<string, NativeOutput> | undefined;
|
|
12
|
+
run: (...bindings: NativeInput[]) => {
|
|
13
|
+
changes: number | bigint;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
transactionSync: SqliteOwner["transactionSync"];
|
|
17
|
+
close: () => void;
|
|
18
|
+
};
|
|
19
|
+
/** The actual Node connection or Local's explicit native Bun adapter. */
|
|
20
|
+
export type PrivateSqliteDatabase = DatabaseSync | PrivateBunSqliteDatabase;
|
|
21
|
+
/**
|
|
22
|
+
* A listing owns this database until its existing finally close. Bun's
|
|
23
|
+
* node:sqlite prepare/close leaves native statements alive until GC. Native
|
|
24
|
+
* close(true) on the native Bun driver explicitly finalizes every statement
|
|
25
|
+
* before releasing the connection, including when a read or decoding fails.
|
|
26
|
+
*/
|
|
27
|
+
export declare function openPrivateReadOnlySqliteDatabase(path: string): PrivateSqliteDatabase;
|
|
28
|
+
/** Writable connections own their statements through deterministic close. */
|
|
29
|
+
export declare function openPrivateWritableSqliteDatabase(path: string, timeout: number): PrivateSqliteDatabase;
|
|
3
30
|
export type PrivateSqliteFailureCode = "busy" | "locked" | "corrupt" | "not_a_database" | "other";
|
|
4
|
-
/** Decode
|
|
31
|
+
/** Decode the maintained drivers' structured result codes before sanitizing. */
|
|
5
32
|
export declare function privateSqliteFailureCode(cause: unknown): PrivateSqliteFailureCode | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* Keep Local's native failure reporting around a host-supplied SQL owner.
|
|
35
|
+
* The supplied owner retains transaction mechanics and connection lifetime.
|
|
36
|
+
*/
|
|
37
|
+
export declare function observePrivateSqliteOwner(owner: SqliteOwner, onFailure?: (code: PrivateSqliteFailureCode) => void): SqliteOwner;
|
|
6
38
|
/** Adapt one synchronous native database to Shiitake's public owner shape. */
|
|
7
|
-
export declare function createPrivateSqliteOwner(database:
|
|
39
|
+
export declare function createPrivateSqliteOwner(database: PrivateSqliteDatabase, onFailure?: (code: PrivateSqliteFailureCode) => void): SqliteOwner;
|
|
@@ -1,19 +1,84 @@
|
|
|
1
1
|
import * as Schema from "effect/Schema";
|
|
2
2
|
import { sqliteRowSchema } from "@fungi.computer/shiitake";
|
|
3
3
|
import { DatabaseSync } from "node:sqlite";
|
|
4
|
+
import { fromNodeSqlite } from "@fungi.computer/watchdog/node-sqlite";
|
|
4
5
|
import { z } from "zod";
|
|
5
6
|
const decodeSqliteRow = Schema.decodeUnknownSync(sqliteRowSchema);
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
const bunSqlite = Object.hasOwn(process.versions, "bun")
|
|
8
|
+
? await import("bun:sqlite")
|
|
9
|
+
: undefined;
|
|
10
|
+
/**
|
|
11
|
+
* A listing owns this database until its existing finally close. Bun's
|
|
12
|
+
* node:sqlite prepare/close leaves native statements alive until GC. Native
|
|
13
|
+
* close(true) on the native Bun driver explicitly finalizes every statement
|
|
14
|
+
* before releasing the connection, including when a read or decoding fails.
|
|
15
|
+
*/
|
|
16
|
+
export function openPrivateReadOnlySqliteDatabase(path) {
|
|
17
|
+
return openPrivateSqliteDatabase(path, true, 0);
|
|
18
|
+
}
|
|
19
|
+
/** Writable connections own their statements through deterministic close. */
|
|
20
|
+
export function openPrivateWritableSqliteDatabase(path, timeout) {
|
|
21
|
+
return openPrivateSqliteDatabase(path, false, timeout);
|
|
22
|
+
}
|
|
23
|
+
function openPrivateSqliteDatabase(path, readOnly, timeout) {
|
|
24
|
+
if (bunSqlite === undefined)
|
|
25
|
+
return new DatabaseSync(path, { readOnly, timeout });
|
|
26
|
+
const database = new bunSqlite.Database(path, {
|
|
27
|
+
readonly: readOnly,
|
|
28
|
+
create: !readOnly,
|
|
29
|
+
strict: true,
|
|
30
|
+
});
|
|
31
|
+
try {
|
|
32
|
+
database.run(`PRAGMA busy_timeout = ${String(timeout)}`);
|
|
33
|
+
if (!readOnly)
|
|
34
|
+
database.run("PRAGMA foreign_keys = ON");
|
|
35
|
+
}
|
|
36
|
+
catch (cause) {
|
|
37
|
+
database.close(true);
|
|
38
|
+
throw cause;
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
get isTransaction() {
|
|
42
|
+
return database.inTransaction;
|
|
43
|
+
},
|
|
44
|
+
exec: (statement) => {
|
|
45
|
+
database.run(statement);
|
|
46
|
+
},
|
|
47
|
+
prepare: (statement) => {
|
|
48
|
+
const query = database.prepare(statement);
|
|
49
|
+
return {
|
|
50
|
+
all: (...bindings) => query.all(...bindings),
|
|
51
|
+
get: (...bindings) => query.get(...bindings) ?? undefined,
|
|
52
|
+
run: (...bindings) => query.run(...bindings),
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
transactionSync: (operation) => database.transaction(operation)(),
|
|
56
|
+
close: () => {
|
|
57
|
+
database.close(true);
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const sqliteFailureSchema = z.union([
|
|
62
|
+
z
|
|
63
|
+
.object({
|
|
64
|
+
code: z.literal("ERR_SQLITE_ERROR"),
|
|
65
|
+
errcode: z.number().int(),
|
|
66
|
+
})
|
|
67
|
+
.transform((failure) => failure.errcode),
|
|
68
|
+
z
|
|
69
|
+
.object({
|
|
70
|
+
code: z.string().startsWith("SQLITE_"),
|
|
71
|
+
errno: z.number().int(),
|
|
72
|
+
})
|
|
73
|
+
.transform((failure) => failure.errno),
|
|
74
|
+
]);
|
|
10
75
|
const sqlitePrimaryCodeMask = 0xff;
|
|
11
|
-
/** Decode
|
|
76
|
+
/** Decode the maintained drivers' structured result codes before sanitizing. */
|
|
12
77
|
export function privateSqliteFailureCode(cause) {
|
|
13
78
|
const parsed = sqliteFailureSchema.safeParse(cause);
|
|
14
79
|
if (!parsed.success)
|
|
15
80
|
return undefined;
|
|
16
|
-
switch (parsed.data
|
|
81
|
+
switch (parsed.data & sqlitePrimaryCodeMask) {
|
|
17
82
|
case 5:
|
|
18
83
|
return "busy";
|
|
19
84
|
case 6:
|
|
@@ -44,8 +109,11 @@ function nativeOutput(value) {
|
|
|
44
109
|
function nativeRow(row) {
|
|
45
110
|
return Object.fromEntries(Object.entries(row).map(([key, value]) => [key, nativeOutput(value)]));
|
|
46
111
|
}
|
|
47
|
-
/**
|
|
48
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Keep Local's native failure reporting around a host-supplied SQL owner.
|
|
114
|
+
* The supplied owner retains transaction mechanics and connection lifetime.
|
|
115
|
+
*/
|
|
116
|
+
export function observePrivateSqliteOwner(owner, onFailure) {
|
|
49
117
|
const observeFailure = (cause) => {
|
|
50
118
|
const code = privateSqliteFailureCode(cause);
|
|
51
119
|
if (code !== undefined)
|
|
@@ -55,17 +123,36 @@ export function createPrivateSqliteOwner(database, onFailure) {
|
|
|
55
123
|
return Object.freeze({
|
|
56
124
|
sql: Object.freeze({
|
|
57
125
|
exec: (statement, ...bindings) => {
|
|
58
|
-
let rows;
|
|
59
126
|
try {
|
|
60
|
-
|
|
61
|
-
const values = bindings.map(nativeInput);
|
|
62
|
-
rows = readerStatement.test(statement)
|
|
63
|
-
? prepared.all(...values).map(nativeRow)
|
|
64
|
-
: (prepared.run(...values), []);
|
|
127
|
+
return owner.sql.exec(statement, ...bindings);
|
|
65
128
|
}
|
|
66
129
|
catch (cause) {
|
|
67
130
|
return observeFailure(cause);
|
|
68
131
|
}
|
|
132
|
+
},
|
|
133
|
+
}),
|
|
134
|
+
transactionSync: (operation) => {
|
|
135
|
+
try {
|
|
136
|
+
return owner.transactionSync(operation);
|
|
137
|
+
}
|
|
138
|
+
catch (cause) {
|
|
139
|
+
return observeFailure(cause);
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
/** Adapt one synchronous native database to Shiitake's public owner shape. */
|
|
145
|
+
export function createPrivateSqliteOwner(database, onFailure) {
|
|
146
|
+
if (database instanceof DatabaseSync)
|
|
147
|
+
return observePrivateSqliteOwner(fromNodeSqlite(database), onFailure);
|
|
148
|
+
return observePrivateSqliteOwner({
|
|
149
|
+
sql: {
|
|
150
|
+
exec: (statement, ...bindings) => {
|
|
151
|
+
const prepared = database.prepare(statement);
|
|
152
|
+
const values = bindings.map(nativeInput);
|
|
153
|
+
const rows = readerStatement.test(statement)
|
|
154
|
+
? prepared.all(...values).map(nativeRow)
|
|
155
|
+
: (prepared.run(...values), []);
|
|
69
156
|
return Object.freeze({
|
|
70
157
|
toArray: () => rows.map((row) => decodeSqliteRow(row)),
|
|
71
158
|
one: () => {
|
|
@@ -76,28 +163,7 @@ export function createPrivateSqliteOwner(database, onFailure) {
|
|
|
76
163
|
},
|
|
77
164
|
});
|
|
78
165
|
},
|
|
79
|
-
}),
|
|
80
|
-
transactionSync: (operation) => {
|
|
81
|
-
try {
|
|
82
|
-
database.exec("BEGIN");
|
|
83
|
-
}
|
|
84
|
-
catch (cause) {
|
|
85
|
-
return observeFailure(cause);
|
|
86
|
-
}
|
|
87
|
-
try {
|
|
88
|
-
const value = operation();
|
|
89
|
-
database.exec("COMMIT");
|
|
90
|
-
return value;
|
|
91
|
-
}
|
|
92
|
-
catch (cause) {
|
|
93
|
-
try {
|
|
94
|
-
database.exec("ROLLBACK");
|
|
95
|
-
}
|
|
96
|
-
catch (rollbackCause) {
|
|
97
|
-
observeFailure(rollbackCause);
|
|
98
|
-
}
|
|
99
|
-
return observeFailure(cause);
|
|
100
|
-
}
|
|
101
166
|
},
|
|
102
|
-
|
|
167
|
+
transactionSync: database.transactionSync,
|
|
168
|
+
}, onFailure);
|
|
103
169
|
}
|
|
@@ -3,8 +3,8 @@ import { z } from "zod";
|
|
|
3
3
|
declare const stateSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
4
4
|
state: z.ZodEnum<{
|
|
5
5
|
running: "running";
|
|
6
|
-
sleeping: "sleeping";
|
|
7
6
|
starting: "starting";
|
|
7
|
+
sleeping: "sleeping";
|
|
8
8
|
}>;
|
|
9
9
|
}, z.core.$strict>, z.ZodObject<{
|
|
10
10
|
state: z.ZodLiteral<"queued">;
|
|
@@ -17,8 +17,8 @@ export type Options = Readonly<{
|
|
|
17
17
|
signal?: AbortSignal;
|
|
18
18
|
}>;
|
|
19
19
|
/** Team directory and real runtime liveness, using Local's existing authenticated MCP transport. */
|
|
20
|
-
export declare function listPrivateTeamComputers(rawEndpoint: string,
|
|
21
|
-
state: "running" | "
|
|
20
|
+
export declare function listPrivateTeamComputers(rawEndpoint: string, input?: Options): Promise<({
|
|
21
|
+
state: "running" | "starting" | "sleeping";
|
|
22
22
|
computerId: string;
|
|
23
23
|
name: string;
|
|
24
24
|
} | {
|
|
@@ -28,7 +28,7 @@ export declare function listPrivateTeamComputers(rawEndpoint: string, options?:
|
|
|
28
28
|
name: string;
|
|
29
29
|
})[]>;
|
|
30
30
|
/** Await admission while reporting live queue positions. Ctrl-C stops the local wait. */
|
|
31
|
-
export declare function startPrivateTeamComputer(rawEndpoint: string, name: string,
|
|
31
|
+
export declare function startPrivateTeamComputer(rawEndpoint: string, name: string, input: Options & Readonly<{
|
|
32
32
|
onState: (state: ComputerState) => void;
|
|
33
33
|
}>): Promise<{
|
|
34
34
|
[x: string]: unknown;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { setTimeout as delay } from "node:timers/promises";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { capturePrivateLocalConfigRoot } from "./private-local-config-root.js";
|
|
3
4
|
import { callFungiMcpTeamControl, fungiMcpTeamEndpoint, } from "./private-fungi-account.js";
|
|
4
5
|
const stateSchema = z.discriminatedUnion("state", [
|
|
5
6
|
z.strictObject({ state: z.enum(["running", "sleeping", "starting"]) }),
|
|
@@ -49,7 +50,11 @@ async function directory(endpoint, options) {
|
|
|
49
50
|
throw new Error("This Team has too many Computer pages to list in one command.");
|
|
50
51
|
}
|
|
51
52
|
/** Team directory and real runtime liveness, using Local's existing authenticated MCP transport. */
|
|
52
|
-
export async function listPrivateTeamComputers(rawEndpoint,
|
|
53
|
+
export async function listPrivateTeamComputers(rawEndpoint, input = {}) {
|
|
54
|
+
const options = {
|
|
55
|
+
...input,
|
|
56
|
+
credentialPath: input.credentialPath ?? capturePrivateLocalConfigRoot().credentialPath,
|
|
57
|
+
};
|
|
53
58
|
const { endpoint } = fungiMcpTeamEndpoint(rawEndpoint);
|
|
54
59
|
const computers = await directory(endpoint, options);
|
|
55
60
|
const rows = [];
|
|
@@ -61,7 +66,11 @@ export async function listPrivateTeamComputers(rawEndpoint, options = {}) {
|
|
|
61
66
|
return rows;
|
|
62
67
|
}
|
|
63
68
|
/** Await admission while reporting live queue positions. Ctrl-C stops the local wait. */
|
|
64
|
-
export async function startPrivateTeamComputer(rawEndpoint, name,
|
|
69
|
+
export async function startPrivateTeamComputer(rawEndpoint, name, input) {
|
|
70
|
+
const options = {
|
|
71
|
+
...input,
|
|
72
|
+
credentialPath: input.credentialPath ?? capturePrivateLocalConfigRoot().credentialPath,
|
|
73
|
+
};
|
|
65
74
|
const { endpoint } = fungiMcpTeamEndpoint(rawEndpoint);
|
|
66
75
|
const computers = await directory(endpoint, options);
|
|
67
76
|
const matches = computers.filter((computer) => computer.computerId === name || computer.name === name);
|