@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,4 @@
|
|
|
1
|
+
import { Cli } from "@fungi.computer/lantern";
|
|
2
|
+
import type { AuthInteraction } from "@earendil-works/pi-ai";
|
|
3
|
+
/** Pi owns provider flows and credentials; Clack owns only their presentation. */
|
|
4
|
+
export declare function linearAuthInteraction(interactive: boolean, cli?: Cli): AuthInteraction;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Cli, isCancel, } from "@fungi.computer/lantern";
|
|
2
|
+
const defaultAuthUrlInstructions = "Waiting for the browser… If it doesn't return, paste the final redirect URL here.";
|
|
3
|
+
/** Pi owns provider flows and credentials; Clack owns only their presentation. */
|
|
4
|
+
export function linearAuthInteraction(interactive, cli = new Cli({ brand: "fungi-local-auth", interactive })) {
|
|
5
|
+
return {
|
|
6
|
+
async prompt(prompt) {
|
|
7
|
+
if (!interactive)
|
|
8
|
+
throw new Error("This choice needs an interactive terminal; pass --provider <id> instead. No credentials were changed.");
|
|
9
|
+
const options = {
|
|
10
|
+
message: prompt.message,
|
|
11
|
+
};
|
|
12
|
+
if (prompt.signal !== undefined)
|
|
13
|
+
options.signal = prompt.signal;
|
|
14
|
+
let answer;
|
|
15
|
+
if (prompt.type === "select") {
|
|
16
|
+
answer = await cli.select({
|
|
17
|
+
...options,
|
|
18
|
+
options: prompt.options.map((option) => {
|
|
19
|
+
const choice = {
|
|
20
|
+
value: option.id,
|
|
21
|
+
label: option.label,
|
|
22
|
+
};
|
|
23
|
+
if (option.description !== undefined)
|
|
24
|
+
choice.hint = option.description;
|
|
25
|
+
return choice;
|
|
26
|
+
}),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
else if (prompt.type === "secret" || prompt.type === "manual_code") {
|
|
30
|
+
answer = await cli.password(options);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
const textOptions = { ...options };
|
|
34
|
+
if (prompt.placeholder !== undefined)
|
|
35
|
+
textOptions.placeholder = prompt.placeholder;
|
|
36
|
+
answer = await cli.text(textOptions);
|
|
37
|
+
}
|
|
38
|
+
if (isCancel(answer))
|
|
39
|
+
throw new Error("Login cancelled.");
|
|
40
|
+
return answer;
|
|
41
|
+
},
|
|
42
|
+
notify(event) {
|
|
43
|
+
switch (event.type) {
|
|
44
|
+
case "auth_url": {
|
|
45
|
+
const heading = event.providerLabel === undefined
|
|
46
|
+
? "Sign in"
|
|
47
|
+
: `Sign in to ${event.providerLabel}`;
|
|
48
|
+
// The hyperlink line carries the whole URL in its OSC 8 escape; the
|
|
49
|
+
// URL itself follows as one framed, unbroken line for copying.
|
|
50
|
+
cli.info(heading);
|
|
51
|
+
cli.info(cli.link("Open sign-in page", event.url));
|
|
52
|
+
cli.url(event.url);
|
|
53
|
+
cli.info(event.instructions === undefined
|
|
54
|
+
? defaultAuthUrlInstructions
|
|
55
|
+
: `Waiting for the browser… ${event.instructions}`);
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
case "device_code":
|
|
59
|
+
cli.info(`Open ${event.verificationUri}\nEnter code: ${event.userCode}`);
|
|
60
|
+
break;
|
|
61
|
+
case "info":
|
|
62
|
+
cli.info([
|
|
63
|
+
event.message,
|
|
64
|
+
...(event.links ?? []).map((link) => link.url),
|
|
65
|
+
].join("\n"));
|
|
66
|
+
break;
|
|
67
|
+
case "progress":
|
|
68
|
+
cli.step(event.message);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Mycelium } from "@fungi.computer/mycelium";
|
|
2
|
+
import { defineExtension, type ExtensionCapabilityV0, type ExtensionGrantV0, type RosteredExtensionV0 } from "@fungi.computer/shiitake/extensions";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
export type LocalExtensionFailureReason = "install" | "manifest" | "artifact" | "not-installed" | "not-enabled" | "no-tools" | "load";
|
|
5
|
+
/** A typed refusal from the extension owner; never reported as usable. */
|
|
6
|
+
export declare class LocalExtensionError extends Error {
|
|
7
|
+
readonly name = "LocalExtensionError";
|
|
8
|
+
readonly reason: LocalExtensionFailureReason;
|
|
9
|
+
constructor(reason: LocalExtensionFailureReason, message: string, options?: ErrorOptions);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The host port an installed extension's `./host` factory receives: the
|
|
13
|
+
* host's own extension SDK instances and this project's private runtime
|
|
14
|
+
* directory, which its terminal programs also receive.
|
|
15
|
+
*/
|
|
16
|
+
export type LocalExtensionHostV1 = Readonly<{
|
|
17
|
+
runtimeDirectory: string;
|
|
18
|
+
defineExtension: typeof defineExtension;
|
|
19
|
+
Mycelium: typeof Mycelium;
|
|
20
|
+
z: typeof z;
|
|
21
|
+
}>;
|
|
22
|
+
/** What an installed package's `./host` entry exports by default. */
|
|
23
|
+
export type LocalExtensionFactoryV1 = (host: LocalExtensionHostV1) => RosteredExtensionV0 | Promise<RosteredExtensionV0>;
|
|
24
|
+
export type LocalExtensionSummary = Readonly<{
|
|
25
|
+
id: string;
|
|
26
|
+
revision: string;
|
|
27
|
+
version: string;
|
|
28
|
+
source: string;
|
|
29
|
+
commit: string | null;
|
|
30
|
+
capabilities: readonly ExtensionCapabilityV0[];
|
|
31
|
+
terminals: readonly string[];
|
|
32
|
+
}>;
|
|
33
|
+
/** One program a project-enabled extension may open in a terminal pane. */
|
|
34
|
+
export type LocalExtensionTerminal = Readonly<{
|
|
35
|
+
extensionId: string;
|
|
36
|
+
terminalId: string;
|
|
37
|
+
name: string;
|
|
38
|
+
description: string;
|
|
39
|
+
/** Absolute executable followed by its arguments. */
|
|
40
|
+
command: readonly string[];
|
|
41
|
+
cwd: string;
|
|
42
|
+
env: Readonly<Record<string, string>>;
|
|
43
|
+
}>;
|
|
44
|
+
export type LocalProjectExtensions = Readonly<{
|
|
45
|
+
/** Absolute client entries for the ordinary client-extension loader. */
|
|
46
|
+
clients: readonly string[];
|
|
47
|
+
terminals: readonly LocalExtensionTerminal[];
|
|
48
|
+
diagnostics: readonly Error[];
|
|
49
|
+
}>;
|
|
50
|
+
export type LocalAgentExtensions = Readonly<{
|
|
51
|
+
roster: readonly RosteredExtensionV0[];
|
|
52
|
+
grants: readonly ExtensionGrantV0[];
|
|
53
|
+
/** Extensions left out of this Agent's roster, and why. */
|
|
54
|
+
diagnostics: readonly Readonly<{
|
|
55
|
+
extensionId: string;
|
|
56
|
+
error: Error;
|
|
57
|
+
}>[];
|
|
58
|
+
}>;
|
|
59
|
+
export type LocalExtensionsOptions = Readonly<{
|
|
60
|
+
/** The user home that owns `.fungi/extensions`; defaults to the OS home. */
|
|
61
|
+
home?: string | undefined;
|
|
62
|
+
}>;
|
|
63
|
+
/** Open the one extension owner for a user home. */
|
|
64
|
+
export declare function localExtensions(options?: LocalExtensionsOptions): {
|
|
65
|
+
root: string;
|
|
66
|
+
/**
|
|
67
|
+
* Install a package with the shipped Bun package manager (lifecycle
|
|
68
|
+
* scripts disabled) and record the exact resolved revision. A Git source
|
|
69
|
+
* is pinned to the commit Bun's lockfile resolved; updating is another
|
|
70
|
+
* explicit `add`. Missing declared artifacts are a refusal.
|
|
71
|
+
*/
|
|
72
|
+
add(source: string): Promise<LocalExtensionSummary>;
|
|
73
|
+
/** Remove the package and every activation that references it. */
|
|
74
|
+
remove(id: string): Promise<boolean>;
|
|
75
|
+
list(): Promise<readonly LocalExtensionSummary[]>;
|
|
76
|
+
/** Enable the current revision's client part and programs for a project. */
|
|
77
|
+
enableProject(projectRoot: string, id: string): Promise<string>;
|
|
78
|
+
disableProject(projectRoot: string, id: string): Promise<boolean>;
|
|
79
|
+
/**
|
|
80
|
+
* Grant one Agent store the current revision's declared capabilities.
|
|
81
|
+
* Running work keeps its captured roster; the Agent applies this when it
|
|
82
|
+
* next opens.
|
|
83
|
+
*/
|
|
84
|
+
enableAgent(storeDirectory: string, id: string): Promise<{
|
|
85
|
+
revision: string;
|
|
86
|
+
capabilities: ("state" | "attention" | "settled" | "send" | "timers" | "commands" | "projection" | "run_tools" | "files" | "prompt" | "compaction_handoff")[];
|
|
87
|
+
}>;
|
|
88
|
+
disableAgent(storeDirectory: string, id: string): Promise<boolean>;
|
|
89
|
+
/** The project's enabled client entries and program launchers. */
|
|
90
|
+
project(projectRoot: string): Promise<LocalProjectExtensions>;
|
|
91
|
+
/**
|
|
92
|
+
* Load the Agent's enabled server extensions through their host factory.
|
|
93
|
+
* Each failure is isolated to its extension and reported.
|
|
94
|
+
*/
|
|
95
|
+
agent(storeDirectory: string, projectRoot: string): Promise<LocalAgentExtensions>;
|
|
96
|
+
};
|
|
97
|
+
export type LocalExtensions = ReturnType<typeof localExtensions>;
|
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { access, constants, mkdir, mkdtemp, readFile, realpath, rename, rm, writeFile, } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join, resolve, sep } from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { Mycelium } from "@fungi.computer/mycelium";
|
|
7
|
+
import { defineExtension, ExtensionId, } from "@fungi.computer/shiitake/extensions";
|
|
8
|
+
import * as Predicate from "effect/Predicate";
|
|
9
|
+
import * as Schema from "effect/Schema";
|
|
10
|
+
import lockfile from "proper-lockfile";
|
|
11
|
+
import writeFileAtomic from "write-file-atomic";
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
/**
|
|
14
|
+
* Installed Local extensions. One owner holds the package bytes under
|
|
15
|
+
* `~/.fungi/extensions`, and the activations its typed operations write. A
|
|
16
|
+
* project's client part and program launchers, and an Agent's tools, are
|
|
17
|
+
* separate activations keyed by canonical path. Nothing under a project or
|
|
18
|
+
* Agent store authorizes anything: a copied folder starts disabled.
|
|
19
|
+
*/
|
|
20
|
+
const capabilities = [
|
|
21
|
+
"state",
|
|
22
|
+
"attention",
|
|
23
|
+
"settled",
|
|
24
|
+
"send",
|
|
25
|
+
"timers",
|
|
26
|
+
"commands",
|
|
27
|
+
"projection",
|
|
28
|
+
"run_tools",
|
|
29
|
+
"files",
|
|
30
|
+
"prompt",
|
|
31
|
+
"compaction_handoff",
|
|
32
|
+
];
|
|
33
|
+
const decodeExtensionId = Schema.decodeUnknownSync(ExtensionId);
|
|
34
|
+
const identifier = z.string().regex(/^[a-z][a-z0-9]*(?:[-_.][a-z0-9]+)*$/u);
|
|
35
|
+
const revisionPattern = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,127}$/u;
|
|
36
|
+
const manifestSchema = z.strictObject({
|
|
37
|
+
id: z.string().transform((value) => decodeExtensionId(value)),
|
|
38
|
+
capabilities: z.array(z.enum(capabilities)).max(10).default([]),
|
|
39
|
+
terminals: z
|
|
40
|
+
.array(z.strictObject({
|
|
41
|
+
id: identifier.max(64),
|
|
42
|
+
name: z.string().trim().min(1).max(64),
|
|
43
|
+
description: z.string().max(200).default(""),
|
|
44
|
+
/** Package-relative executable and its arguments. */
|
|
45
|
+
command: z
|
|
46
|
+
.array(z.string().min(1).max(1_024))
|
|
47
|
+
.min(1)
|
|
48
|
+
.max(32)
|
|
49
|
+
.refine((command) => command[0]?.startsWith("./") === true, {
|
|
50
|
+
message: "Terminal commands run a package-relative executable",
|
|
51
|
+
}),
|
|
52
|
+
}))
|
|
53
|
+
.max(8)
|
|
54
|
+
.default([]),
|
|
55
|
+
});
|
|
56
|
+
const packageSchema = z.looseObject({
|
|
57
|
+
name: z.string().min(1).max(214),
|
|
58
|
+
version: z.string().min(1).max(64),
|
|
59
|
+
/** Fungi entries are plain subpath exports. */
|
|
60
|
+
exports: z.record(z.string(), z.string()).default({}),
|
|
61
|
+
fungi: manifestSchema,
|
|
62
|
+
});
|
|
63
|
+
const terminalRecord = z.strictObject({
|
|
64
|
+
id: z.string(),
|
|
65
|
+
name: z.string(),
|
|
66
|
+
description: z.string(),
|
|
67
|
+
command: z.array(z.string()),
|
|
68
|
+
});
|
|
69
|
+
const revisionRecord = z.strictObject({
|
|
70
|
+
name: z.string(),
|
|
71
|
+
version: z.string(),
|
|
72
|
+
source: z.string(),
|
|
73
|
+
resolution: z.string(),
|
|
74
|
+
commit: z.string().nullable(),
|
|
75
|
+
directory: z.string(),
|
|
76
|
+
capabilities: z.array(z.enum(capabilities)),
|
|
77
|
+
terminals: z.array(terminalRecord),
|
|
78
|
+
host: z.string().nullable(),
|
|
79
|
+
client: z.string().nullable(),
|
|
80
|
+
});
|
|
81
|
+
const installedSchema = z.strictObject({
|
|
82
|
+
extensions: z.record(z.string(), z.strictObject({
|
|
83
|
+
current: z.string(),
|
|
84
|
+
revisions: z.record(z.string(), revisionRecord),
|
|
85
|
+
})),
|
|
86
|
+
});
|
|
87
|
+
const activationsSchema = z.strictObject({
|
|
88
|
+
projects: z.record(z.string(), z.record(z.string(), z.string())),
|
|
89
|
+
agents: z.record(z.string(), z.record(z.string(), z.strictObject({
|
|
90
|
+
revision: z.string(),
|
|
91
|
+
capabilities: z.array(z.enum(capabilities)),
|
|
92
|
+
}))),
|
|
93
|
+
});
|
|
94
|
+
const lockSchema = z.looseObject({
|
|
95
|
+
packages: z.record(z.string(), z.array(z.unknown()).min(1)),
|
|
96
|
+
});
|
|
97
|
+
const hostModuleSchema = z.looseObject({
|
|
98
|
+
default: z.custom((value) => Predicate.isFunction(value), { message: "An extension host entry exports a default factory" }),
|
|
99
|
+
});
|
|
100
|
+
const rosteredSchema = z.looseObject({
|
|
101
|
+
manifest: z.looseObject({
|
|
102
|
+
id: z.string(),
|
|
103
|
+
requests: z.array(z.enum(capabilities)),
|
|
104
|
+
}),
|
|
105
|
+
});
|
|
106
|
+
/** A typed refusal from the extension owner; never reported as usable. */
|
|
107
|
+
export class LocalExtensionError extends Error {
|
|
108
|
+
name = "LocalExtensionError";
|
|
109
|
+
reason;
|
|
110
|
+
constructor(reason, message, options) {
|
|
111
|
+
super(message, options);
|
|
112
|
+
this.reason = reason;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function failure(cause) {
|
|
116
|
+
return cause instanceof Error ? cause : new Error(String(cause));
|
|
117
|
+
}
|
|
118
|
+
function absent(cause) {
|
|
119
|
+
return (cause instanceof Error &&
|
|
120
|
+
"code" in cause &&
|
|
121
|
+
(cause.code === "ENOENT" || cause.code === "ENOTDIR"));
|
|
122
|
+
}
|
|
123
|
+
/** Open the one extension owner for a user home. */
|
|
124
|
+
export function localExtensions(options = {}) {
|
|
125
|
+
const root = join(resolve(options.home ?? homedir()), ".fungi", "extensions");
|
|
126
|
+
const installedPath = join(root, "installed.json");
|
|
127
|
+
const activationsPath = join(root, "activations.json");
|
|
128
|
+
async function readJson(path, schema, empty) {
|
|
129
|
+
let text;
|
|
130
|
+
try {
|
|
131
|
+
text = await readFile(path, "utf8");
|
|
132
|
+
}
|
|
133
|
+
catch (cause) {
|
|
134
|
+
if (absent(cause))
|
|
135
|
+
return empty;
|
|
136
|
+
throw cause;
|
|
137
|
+
}
|
|
138
|
+
return schema.parse(JSON.parse(text));
|
|
139
|
+
}
|
|
140
|
+
const readInstalled = () => readJson(installedPath, installedSchema, { extensions: {} });
|
|
141
|
+
const readActivations = () => readJson(activationsPath, activationsSchema, { projects: {}, agents: {} });
|
|
142
|
+
/** Serialize owner writes across CLI and service processes. */
|
|
143
|
+
async function update(change) {
|
|
144
|
+
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
145
|
+
const release = await lockfile.lock(root, {
|
|
146
|
+
realpath: false,
|
|
147
|
+
lockfilePath: join(root, ".owner.lock"),
|
|
148
|
+
stale: 30_000,
|
|
149
|
+
update: 10_000,
|
|
150
|
+
retries: { retries: 20, factor: 1.2, minTimeout: 25, maxTimeout: 100 },
|
|
151
|
+
});
|
|
152
|
+
try {
|
|
153
|
+
const installed = await readInstalled();
|
|
154
|
+
const activations = await readActivations();
|
|
155
|
+
const result = change(installed, activations);
|
|
156
|
+
const options = { encoding: "utf8", fsync: true, mode: 0o600 };
|
|
157
|
+
await writeFileAtomic(installedPath, `${JSON.stringify(installed, null, 2)}\n`, options);
|
|
158
|
+
await writeFileAtomic(activationsPath, `${JSON.stringify(activations, null, 2)}\n`, options);
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
await release();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function currentRevision(installed, id) {
|
|
166
|
+
const entry = installed.extensions[id];
|
|
167
|
+
const revision = entry === undefined ? undefined : entry.revisions[entry.current];
|
|
168
|
+
if (entry === undefined || revision === undefined)
|
|
169
|
+
throw new LocalExtensionError("not-installed", `Extension ${id} is not installed`);
|
|
170
|
+
return { revision: entry.current, record: revision };
|
|
171
|
+
}
|
|
172
|
+
/** A 0700 directory shared by one project's programs and Agent tools. */
|
|
173
|
+
async function runtimeDirectory(projectRoot, id) {
|
|
174
|
+
const project = createHash("sha256")
|
|
175
|
+
.update(await realpath(projectRoot))
|
|
176
|
+
.digest("hex")
|
|
177
|
+
.slice(0, 16);
|
|
178
|
+
const directory = join(root, "run", project, id);
|
|
179
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
180
|
+
return directory;
|
|
181
|
+
}
|
|
182
|
+
/** A declared `./host` or `./client` export must exist in the installed package. */
|
|
183
|
+
async function declaredEntry(directory, exports, subpath) {
|
|
184
|
+
const target = exports[subpath];
|
|
185
|
+
if (target === undefined)
|
|
186
|
+
return null;
|
|
187
|
+
const path = resolve(directory, target);
|
|
188
|
+
if (!path.startsWith(`${directory}${sep}`))
|
|
189
|
+
throw new LocalExtensionError("manifest", `${subpath} must point inside its package`);
|
|
190
|
+
try {
|
|
191
|
+
await access(path, constants.R_OK);
|
|
192
|
+
}
|
|
193
|
+
catch (cause) {
|
|
194
|
+
throw new LocalExtensionError("artifact", `The package declares ${subpath} but ${target} is not built`, { cause });
|
|
195
|
+
}
|
|
196
|
+
return path;
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
root,
|
|
200
|
+
/**
|
|
201
|
+
* Install a package with the shipped Bun package manager (lifecycle
|
|
202
|
+
* scripts disabled) and record the exact resolved revision. A Git source
|
|
203
|
+
* is pinned to the commit Bun's lockfile resolved; updating is another
|
|
204
|
+
* explicit `add`. Missing declared artifacts are a refusal.
|
|
205
|
+
*/
|
|
206
|
+
async add(source) {
|
|
207
|
+
await mkdir(join(root, "staging"), { recursive: true, mode: 0o700 });
|
|
208
|
+
const staging = await mkdtemp(join(root, "staging", "install-"));
|
|
209
|
+
try {
|
|
210
|
+
await writeFile(join(staging, "package.json"), `${JSON.stringify({ private: true })}\n`);
|
|
211
|
+
const child = Bun.spawn([process.execPath, "add", "--ignore-scripts", source], {
|
|
212
|
+
cwd: staging,
|
|
213
|
+
// A private cache resolves the named ref afresh instead of a
|
|
214
|
+
// cached checkout of an earlier commit.
|
|
215
|
+
env: {
|
|
216
|
+
...process.env,
|
|
217
|
+
BUN_BE_BUN: "1",
|
|
218
|
+
BUN_INSTALL_CACHE_DIR: join(staging, ".install-cache"),
|
|
219
|
+
},
|
|
220
|
+
stdout: "pipe",
|
|
221
|
+
stderr: "pipe",
|
|
222
|
+
});
|
|
223
|
+
const [status, stderr] = await Promise.all([
|
|
224
|
+
child.exited,
|
|
225
|
+
new Response(child.stderr).text(),
|
|
226
|
+
]);
|
|
227
|
+
if (status !== 0)
|
|
228
|
+
throw new LocalExtensionError("install", `Could not install ${source}: ${stderr.trim().slice(0, 2_000)}`);
|
|
229
|
+
const lock = lockSchema.parse(Bun.JSONC.parse(await readFile(join(staging, "bun.lock"), "utf8")));
|
|
230
|
+
const dependencies = Object.keys(z
|
|
231
|
+
.looseObject({ dependencies: z.record(z.string(), z.string()) })
|
|
232
|
+
.parse(JSON.parse(await readFile(join(staging, "package.json"), "utf8"))).dependencies);
|
|
233
|
+
const name = dependencies[0];
|
|
234
|
+
if (name === undefined || dependencies.length !== 1)
|
|
235
|
+
throw new LocalExtensionError("install", `${source} did not install exactly one package`);
|
|
236
|
+
const directory = join(staging, "node_modules", ...name.split("/"));
|
|
237
|
+
const pkg = packageSchema.safeParse(JSON.parse(await readFile(join(directory, "package.json"), "utf8")));
|
|
238
|
+
if (!pkg.success)
|
|
239
|
+
throw new LocalExtensionError("manifest", `${source} does not declare a Fungi extension in package.json "fungi"`, { cause: pkg.error });
|
|
240
|
+
const selected = { name, directory, pkg: pkg.data };
|
|
241
|
+
const entry = lock.packages[selected.name] ?? [];
|
|
242
|
+
const resolution = z.string().parse(entry[0]);
|
|
243
|
+
const pinned = z.string().safeParse(entry[2]);
|
|
244
|
+
const git = /#([0-9a-f]{40})$/u.exec(resolution);
|
|
245
|
+
const commit = git?.[1] ?? null;
|
|
246
|
+
const manifest = selected.pkg.fungi;
|
|
247
|
+
const host = await declaredEntry(selected.directory, selected.pkg.exports, "./host");
|
|
248
|
+
const client = await declaredEntry(selected.directory, selected.pkg.exports, "./client");
|
|
249
|
+
if (manifest.capabilities.length > 0 && host === null)
|
|
250
|
+
throw new LocalExtensionError("artifact", `${manifest.id} declares Agent capabilities but its package exports no ./host entry`);
|
|
251
|
+
for (const terminal of manifest.terminals) {
|
|
252
|
+
const executable = resolve(selected.directory, terminal.command[0] ?? "");
|
|
253
|
+
if (!executable.startsWith(`${selected.directory}${sep}`))
|
|
254
|
+
throw new LocalExtensionError("manifest", `Terminal ${terminal.id} must run a file inside its package`);
|
|
255
|
+
try {
|
|
256
|
+
await access(executable, constants.X_OK);
|
|
257
|
+
}
|
|
258
|
+
catch (cause) {
|
|
259
|
+
throw new LocalExtensionError("artifact", `Terminal ${terminal.id} program ${terminal.command[0] ?? ""} is not an executable file in the installed package`, { cause });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const revision = commit ??
|
|
263
|
+
(pinned.success
|
|
264
|
+
? `${selected.pkg.version}-${createHash("sha256").update(pinned.data).digest("hex").slice(0, 12)}`
|
|
265
|
+
: selected.pkg.version);
|
|
266
|
+
if (!revisionPattern.test(revision))
|
|
267
|
+
throw new LocalExtensionError("manifest", "Installed revision is not representable");
|
|
268
|
+
const target = join(root, "packages", manifest.id, revision);
|
|
269
|
+
const packageDirectory = join(target, "node_modules", ...selected.name.split("/"));
|
|
270
|
+
const record = {
|
|
271
|
+
name: selected.name,
|
|
272
|
+
version: selected.pkg.version,
|
|
273
|
+
source,
|
|
274
|
+
resolution,
|
|
275
|
+
commit,
|
|
276
|
+
directory: packageDirectory,
|
|
277
|
+
capabilities: manifest.capabilities,
|
|
278
|
+
terminals: manifest.terminals,
|
|
279
|
+
host: host === null
|
|
280
|
+
? null
|
|
281
|
+
: join(target, host.slice(staging.length + sep.length)),
|
|
282
|
+
client: client === null
|
|
283
|
+
? null
|
|
284
|
+
: join(target, client.slice(staging.length + sep.length)),
|
|
285
|
+
};
|
|
286
|
+
await update((installed) => {
|
|
287
|
+
const existing = installed.extensions[manifest.id];
|
|
288
|
+
installed.extensions[manifest.id] = {
|
|
289
|
+
current: revision,
|
|
290
|
+
revisions: { ...existing?.revisions, [revision]: record },
|
|
291
|
+
};
|
|
292
|
+
});
|
|
293
|
+
await mkdir(join(root, "packages", manifest.id), { recursive: true });
|
|
294
|
+
await rm(target, { recursive: true, force: true });
|
|
295
|
+
await rename(staging, target);
|
|
296
|
+
return {
|
|
297
|
+
id: manifest.id,
|
|
298
|
+
revision,
|
|
299
|
+
version: record.version,
|
|
300
|
+
source,
|
|
301
|
+
commit,
|
|
302
|
+
capabilities: record.capabilities,
|
|
303
|
+
terminals: record.terminals.map((terminal) => terminal.id),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
finally {
|
|
307
|
+
await rm(staging, { recursive: true, force: true });
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
/** Remove the package and every activation that references it. */
|
|
311
|
+
async remove(id) {
|
|
312
|
+
const removed = await update((installed, activations) => {
|
|
313
|
+
if (installed.extensions[id] === undefined)
|
|
314
|
+
return false;
|
|
315
|
+
Reflect.deleteProperty(installed.extensions, id);
|
|
316
|
+
for (const project of Object.values(activations.projects))
|
|
317
|
+
Reflect.deleteProperty(project, id);
|
|
318
|
+
for (const agent of Object.values(activations.agents))
|
|
319
|
+
Reflect.deleteProperty(agent, id);
|
|
320
|
+
return true;
|
|
321
|
+
});
|
|
322
|
+
// Loaded code and running programs keep their open files until their
|
|
323
|
+
// own owners end; new owners can no longer select this package.
|
|
324
|
+
await rm(join(root, "packages", id), { recursive: true, force: true });
|
|
325
|
+
return removed;
|
|
326
|
+
},
|
|
327
|
+
async list() {
|
|
328
|
+
const installed = await readInstalled();
|
|
329
|
+
return Object.entries(installed.extensions).flatMap(([id, entry]) => {
|
|
330
|
+
const record = entry.revisions[entry.current];
|
|
331
|
+
return record === undefined
|
|
332
|
+
? []
|
|
333
|
+
: [
|
|
334
|
+
{
|
|
335
|
+
id,
|
|
336
|
+
revision: entry.current,
|
|
337
|
+
version: record.version,
|
|
338
|
+
source: record.source,
|
|
339
|
+
commit: record.commit,
|
|
340
|
+
capabilities: record.capabilities,
|
|
341
|
+
terminals: record.terminals.map((terminal) => terminal.id),
|
|
342
|
+
},
|
|
343
|
+
];
|
|
344
|
+
});
|
|
345
|
+
},
|
|
346
|
+
/** Enable the current revision's client part and programs for a project. */
|
|
347
|
+
async enableProject(projectRoot, id) {
|
|
348
|
+
const project = await realpath(projectRoot);
|
|
349
|
+
return update((installed, activations) => {
|
|
350
|
+
const { revision } = currentRevision(installed, id);
|
|
351
|
+
activations.projects[project] = {
|
|
352
|
+
...activations.projects[project],
|
|
353
|
+
[id]: revision,
|
|
354
|
+
};
|
|
355
|
+
return revision;
|
|
356
|
+
});
|
|
357
|
+
},
|
|
358
|
+
async disableProject(projectRoot, id) {
|
|
359
|
+
const project = await realpath(projectRoot);
|
|
360
|
+
return update((_installed, activations) => {
|
|
361
|
+
const enabled = activations.projects[project];
|
|
362
|
+
if (enabled?.[id] === undefined)
|
|
363
|
+
return false;
|
|
364
|
+
Reflect.deleteProperty(enabled, id);
|
|
365
|
+
return true;
|
|
366
|
+
});
|
|
367
|
+
},
|
|
368
|
+
/**
|
|
369
|
+
* Grant one Agent store the current revision's declared capabilities.
|
|
370
|
+
* Running work keeps its captured roster; the Agent applies this when it
|
|
371
|
+
* next opens.
|
|
372
|
+
*/
|
|
373
|
+
async enableAgent(storeDirectory, id) {
|
|
374
|
+
const store = await realpath(storeDirectory);
|
|
375
|
+
return update((installed, activations) => {
|
|
376
|
+
const { revision, record } = currentRevision(installed, id);
|
|
377
|
+
if (record.host === null || record.capabilities.length === 0)
|
|
378
|
+
throw new LocalExtensionError("no-tools", `Extension ${id} declares no Agent capabilities`);
|
|
379
|
+
activations.agents[store] = {
|
|
380
|
+
...activations.agents[store],
|
|
381
|
+
[id]: { revision, capabilities: record.capabilities },
|
|
382
|
+
};
|
|
383
|
+
return { revision, capabilities: record.capabilities };
|
|
384
|
+
});
|
|
385
|
+
},
|
|
386
|
+
async disableAgent(storeDirectory, id) {
|
|
387
|
+
const store = await realpath(storeDirectory);
|
|
388
|
+
return update((_installed, activations) => {
|
|
389
|
+
const enabled = activations.agents[store];
|
|
390
|
+
if (enabled?.[id] === undefined)
|
|
391
|
+
return false;
|
|
392
|
+
Reflect.deleteProperty(enabled, id);
|
|
393
|
+
return true;
|
|
394
|
+
});
|
|
395
|
+
},
|
|
396
|
+
/** The project's enabled client entries and program launchers. */
|
|
397
|
+
async project(projectRoot) {
|
|
398
|
+
const project = await realpath(projectRoot);
|
|
399
|
+
const [installed, activations] = await Promise.all([
|
|
400
|
+
readInstalled(),
|
|
401
|
+
readActivations(),
|
|
402
|
+
]);
|
|
403
|
+
const clients = [];
|
|
404
|
+
const terminals = [];
|
|
405
|
+
const diagnostics = [];
|
|
406
|
+
for (const [id, revision] of Object.entries(activations.projects[project] ?? {})) {
|
|
407
|
+
const record = installed.extensions[id]?.revisions[revision];
|
|
408
|
+
if (record === undefined) {
|
|
409
|
+
diagnostics.push(new LocalExtensionError("not-installed", `Extension ${id}@${revision} is enabled but not installed`));
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
if (record.client !== null)
|
|
413
|
+
clients.push(record.client);
|
|
414
|
+
const runtime = await runtimeDirectory(project, id);
|
|
415
|
+
for (const terminal of record.terminals)
|
|
416
|
+
terminals.push({
|
|
417
|
+
extensionId: id,
|
|
418
|
+
terminalId: terminal.id,
|
|
419
|
+
name: terminal.name,
|
|
420
|
+
description: terminal.description,
|
|
421
|
+
command: [
|
|
422
|
+
resolve(record.directory, terminal.command[0] ?? ""),
|
|
423
|
+
...terminal.command.slice(1),
|
|
424
|
+
],
|
|
425
|
+
cwd: project,
|
|
426
|
+
env: {
|
|
427
|
+
FUNGI_EXTENSION_ID: id,
|
|
428
|
+
FUNGI_PROJECT_ROOT: project,
|
|
429
|
+
FUNGI_EXTENSION_RUNTIME_DIR: runtime,
|
|
430
|
+
},
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
return { clients, terminals, diagnostics };
|
|
434
|
+
},
|
|
435
|
+
/**
|
|
436
|
+
* Load the Agent's enabled server extensions through their host factory.
|
|
437
|
+
* Each failure is isolated to its extension and reported.
|
|
438
|
+
*/
|
|
439
|
+
async agent(storeDirectory, projectRoot) {
|
|
440
|
+
const store = await realpath(storeDirectory);
|
|
441
|
+
const [installed, activations] = await Promise.all([
|
|
442
|
+
readInstalled(),
|
|
443
|
+
readActivations(),
|
|
444
|
+
]);
|
|
445
|
+
const roster = [];
|
|
446
|
+
const grants = [];
|
|
447
|
+
const diagnostics = [];
|
|
448
|
+
for (const [id, activation] of Object.entries(activations.agents[store] ?? {})) {
|
|
449
|
+
const record = installed.extensions[id]?.revisions[activation.revision];
|
|
450
|
+
try {
|
|
451
|
+
if (record?.host === null || record === undefined)
|
|
452
|
+
throw new LocalExtensionError("not-installed", `Extension ${id}@${activation.revision} is enabled but its host entry is not installed`);
|
|
453
|
+
const module = hostModuleSchema.parse(await import(pathToFileURL(record.host).href));
|
|
454
|
+
const extension = await module.default({
|
|
455
|
+
runtimeDirectory: await runtimeDirectory(projectRoot, id),
|
|
456
|
+
defineExtension,
|
|
457
|
+
Mycelium,
|
|
458
|
+
z,
|
|
459
|
+
});
|
|
460
|
+
const parsed = rosteredSchema.parse(extension);
|
|
461
|
+
if (parsed.manifest.id !== id)
|
|
462
|
+
throw new LocalExtensionError("load", `Extension ${id} host returned ${parsed.manifest.id}`);
|
|
463
|
+
const requested = new Set(parsed.manifest.requests);
|
|
464
|
+
roster.push(z
|
|
465
|
+
.custom((value) => rosteredSchema.safeParse(value).success)
|
|
466
|
+
.parse(extension));
|
|
467
|
+
grants.push({
|
|
468
|
+
id: decodeExtensionId(id),
|
|
469
|
+
capabilities: activation.capabilities.filter((capability) => requested.has(capability)),
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
catch (cause) {
|
|
473
|
+
diagnostics.push({
|
|
474
|
+
extensionId: id,
|
|
475
|
+
error: cause instanceof LocalExtensionError
|
|
476
|
+
? cause
|
|
477
|
+
: new LocalExtensionError("load", `Extension ${id} could not load`, { cause: failure(cause) }),
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return { roster, grants, diagnostics };
|
|
482
|
+
},
|
|
483
|
+
};
|
|
484
|
+
}
|