@fungi.computer/truffle 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
@@ -21554,19 +21554,9 @@ function parseCatalog(input) {
|
|
|
21554
21554
|
}
|
|
21555
21555
|
}
|
|
21556
21556
|
const preparedOperations = operations.map((operation) => prepareOperation(operation));
|
|
21557
|
-
const namespaces = /* @__PURE__ */ new Map();
|
|
21558
21557
|
for (const operation of preparedOperations) {
|
|
21559
|
-
|
|
21560
|
-
|
|
21561
|
-
group.push(operation);
|
|
21562
|
-
namespaces.set(namespace, group);
|
|
21563
|
-
}
|
|
21564
|
-
for (const [namespace, group] of namespaces) {
|
|
21565
|
-
const rendered = group.map((operation) => operation.rendered);
|
|
21566
|
-
const index = renderNamespaceIndex(rendered);
|
|
21567
|
-
const full = renderOperationTypes(rendered);
|
|
21568
|
-
if (index.length > maximumDescriptionTypes || full.length > maximumDescriptionTypes) {
|
|
21569
|
-
throw new ForageError("invalid_input", `Namespace description exceeds the bounded output: ${namespace}`);
|
|
21558
|
+
if (renderOperationTypes([operation.rendered]).length > maximumDescriptionTypes) {
|
|
21559
|
+
throw new ForageError("invalid_input", `Operation description exceeds the bounded output: ${operation.path}`);
|
|
21570
21560
|
}
|
|
21571
21561
|
}
|
|
21572
21562
|
return {
|
|
@@ -27027,6 +27017,10 @@ var GuestThrew = class extends TaggedError2("GuestThrew") {
|
|
|
27027
27017
|
};
|
|
27028
27018
|
var DeadlineExceeded = class extends TaggedError2("DeadlineExceeded") {
|
|
27029
27019
|
};
|
|
27020
|
+
var SandboxBusy = class extends TaggedError2("SandboxBusy") {
|
|
27021
|
+
/** Stable contention code, not a sandbox health failure. */
|
|
27022
|
+
code = "sandbox-busy";
|
|
27023
|
+
};
|
|
27030
27024
|
var Cancelled = class extends TaggedError2("Cancelled") {
|
|
27031
27025
|
};
|
|
27032
27026
|
var DispatchLost = class extends TaggedError2("DispatchLost") {
|
|
@@ -27064,6 +27058,9 @@ function sandboxFailureMessage(failure, includeReference = true) {
|
|
|
27064
27058
|
case "DeadlineExceeded":
|
|
27065
27059
|
text = `execute hit its ${String(failure.limitMs)} ms deadline after ${String(failure.elapsedMs)} ms; split the work or persist progress${failure.acknowledged === false ? "; sandbox did not acknowledge cancellation" : ""}`;
|
|
27066
27060
|
break;
|
|
27061
|
+
case "SandboxBusy":
|
|
27062
|
+
text = `sandbox-busy: all ${String(failure.capacity)} sandbox slots remained occupied after waiting ${String(failure.waitedMs)} ms. Nothing ran; retry this call after capacity becomes available.`;
|
|
27063
|
+
break;
|
|
27067
27064
|
case "Cancelled":
|
|
27068
27065
|
text = failure.acknowledged === false ? "Sandbox did not acknowledge cancellation" : "Execution was cancelled";
|
|
27069
27066
|
break;
|
|
@@ -27096,6 +27093,7 @@ var ownedFailureSchema = external_exports.lazy(() => external_exports.union([
|
|
|
27096
27093
|
external_exports.instanceof(SourceInvalid),
|
|
27097
27094
|
external_exports.instanceof(GuestThrew),
|
|
27098
27095
|
external_exports.instanceof(DeadlineExceeded),
|
|
27096
|
+
external_exports.instanceof(SandboxBusy),
|
|
27099
27097
|
external_exports.instanceof(Cancelled),
|
|
27100
27098
|
external_exports.instanceof(DispatchLost),
|
|
27101
27099
|
external_exports.instanceof(SandboxCrashed),
|
|
@@ -27207,6 +27205,7 @@ var transportEnvelopeSchema = external_exports.lazy(() => external_exports.stric
|
|
|
27207
27205
|
"SourceInvalid",
|
|
27208
27206
|
"GuestThrew",
|
|
27209
27207
|
"DeadlineExceeded",
|
|
27208
|
+
"SandboxBusy",
|
|
27210
27209
|
"Cancelled",
|
|
27211
27210
|
"DispatchLost",
|
|
27212
27211
|
"SandboxCrashed",
|
|
@@ -2066,7 +2066,7 @@
|
|
|
2066
2066
|
"format": "esm"
|
|
2067
2067
|
},
|
|
2068
2068
|
"../../packages/forage/dist/catalog.js": {
|
|
2069
|
-
"bytes":
|
|
2069
|
+
"bytes": 4793,
|
|
2070
2070
|
"imports": [
|
|
2071
2071
|
{
|
|
2072
2072
|
"path": "../../packages/forage/dist/errors.js",
|
|
@@ -6156,7 +6156,7 @@
|
|
|
6156
6156
|
"format": "esm"
|
|
6157
6157
|
},
|
|
6158
6158
|
"../../packages/mycelium/dist/sandbox-failure.js": {
|
|
6159
|
-
"bytes":
|
|
6159
|
+
"bytes": 7648,
|
|
6160
6160
|
"imports": [
|
|
6161
6161
|
{
|
|
6162
6162
|
"path": "../../node_modules/.pnpm/effect@4.0.2/node_modules/effect/dist/Data.js",
|
|
@@ -6167,7 +6167,7 @@
|
|
|
6167
6167
|
"format": "esm"
|
|
6168
6168
|
},
|
|
6169
6169
|
"../../packages/mycelium/dist/sandbox-boundary.js": {
|
|
6170
|
-
"bytes":
|
|
6170
|
+
"bytes": 13388,
|
|
6171
6171
|
"imports": [
|
|
6172
6172
|
{
|
|
6173
6173
|
"path": "../../packages/cairn/dist/index.js",
|
|
@@ -6558,7 +6558,7 @@
|
|
|
6558
6558
|
"bytesInOutput": 1793
|
|
6559
6559
|
},
|
|
6560
6560
|
"../../packages/forage/dist/catalog.js": {
|
|
6561
|
-
"bytesInOutput":
|
|
6561
|
+
"bytesInOutput": 3677
|
|
6562
6562
|
},
|
|
6563
6563
|
"../../packages/forage/dist/app-api.js": {
|
|
6564
6564
|
"bytesInOutput": 1649
|
|
@@ -6708,10 +6708,10 @@
|
|
|
6708
6708
|
"bytesInOutput": 9993
|
|
6709
6709
|
},
|
|
6710
6710
|
"../../packages/mycelium/dist/sandbox-failure.js": {
|
|
6711
|
-
"bytesInOutput":
|
|
6711
|
+
"bytesInOutput": 3670
|
|
6712
6712
|
},
|
|
6713
6713
|
"../../packages/mycelium/dist/sandbox-boundary.js": {
|
|
6714
|
-
"bytesInOutput":
|
|
6714
|
+
"bytesInOutput": 6432
|
|
6715
6715
|
},
|
|
6716
6716
|
"../../packages/mycelium/dist/sandbox-guest.js": {
|
|
6717
6717
|
"bytesInOutput": 4954
|
|
@@ -6720,7 +6720,7 @@
|
|
|
6720
6720
|
"bytesInOutput": 4648
|
|
6721
6721
|
}
|
|
6722
6722
|
},
|
|
6723
|
-
"bytes":
|
|
6723
|
+
"bytes": 993376
|
|
6724
6724
|
}
|
|
6725
6725
|
}
|
|
6726
6726
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ExtensionGrantV0, RosteredExtensionV0 } from "@fungi.computer/shiitake/extensions";
|
|
2
|
+
/** What the Local roster gains for the compaction archive. */
|
|
3
|
+
export type LocalArchiveHandoff = Readonly<{
|
|
4
|
+
roster: readonly RosteredExtensionV0[];
|
|
5
|
+
grants: readonly ExtensionGrantV0[];
|
|
6
|
+
}>;
|
|
7
|
+
/**
|
|
8
|
+
* Zookeeper's compaction handoff over files beside the Agent store. An
|
|
9
|
+
* installed extension the owner chose keeps the single `compaction_handoff`
|
|
10
|
+
* slot: Zookeeper is then skipped and the skip is reported, never a failure.
|
|
11
|
+
*/
|
|
12
|
+
export declare function localArchiveHandoff(storeDirectory: string, installedGrants: readonly ExtensionGrantV0[], report?: (operation: string, cause: unknown) => void): LocalArchiveHandoff;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { zookeeperCompaction, zookeeperCompactionGrant, } from "@fungi.computer/zookeeper";
|
|
2
|
+
import { logEvent } from "./log-event.js";
|
|
3
|
+
import { localArchiveWriter } from "./local-session-archives.js";
|
|
4
|
+
/**
|
|
5
|
+
* Zookeeper's compaction handoff over files beside the Agent store. An
|
|
6
|
+
* installed extension the owner chose keeps the single `compaction_handoff`
|
|
7
|
+
* slot: Zookeeper is then skipped and the skip is reported, never a failure.
|
|
8
|
+
*/
|
|
9
|
+
export function localArchiveHandoff(storeDirectory, installedGrants, report = logEvent) {
|
|
10
|
+
const holder = installedGrants.find(({ capabilities }) => capabilities.includes("compaction_handoff"));
|
|
11
|
+
if (holder !== undefined) {
|
|
12
|
+
report("local-zookeeper-skipped", new Error(`Installed extension ${holder.id} holds the compaction handoff; Zookeeper archives are off for this Agent`));
|
|
13
|
+
return { roster: [], grants: [] };
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
roster: [
|
|
17
|
+
zookeeperCompaction({ writer: localArchiveWriter(storeDirectory) }),
|
|
18
|
+
],
|
|
19
|
+
grants: [zookeeperCompactionGrant],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Mycelium } from "@fungi.computer/mycelium";
|
|
2
2
|
import { defineExtension, type ExtensionCapabilityV0, type ExtensionGrantV0, type RosteredExtensionV0 } from "@fungi.computer/shiitake/extensions";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
|
|
4
|
+
import { type ExtensionSkillFolder } from "./local-skill-source.js";
|
|
5
|
+
export type LocalExtensionFailureReason = "install" | "manifest" | "artifact" | "not-installed" | "not-enabled" | "no-tools" | "no-skills" | "not-held" | "load";
|
|
5
6
|
/** A typed refusal from the extension owner; never reported as usable. */
|
|
6
7
|
export declare class LocalExtensionError extends Error {
|
|
7
8
|
readonly name = "LocalExtensionError";
|
|
@@ -21,6 +22,10 @@ export type LocalExtensionHostV1 = Readonly<{
|
|
|
21
22
|
}>;
|
|
22
23
|
/** What an installed package's `./host` entry exports by default. */
|
|
23
24
|
export type LocalExtensionFactoryV1 = (host: LocalExtensionHostV1) => RosteredExtensionV0 | Promise<RosteredExtensionV0>;
|
|
25
|
+
/** What `add` reports: the summary plus the names of the skills it ships. */
|
|
26
|
+
export type LocalInstalledExtension = LocalExtensionSummary & Readonly<{
|
|
27
|
+
skills: readonly string[];
|
|
28
|
+
}>;
|
|
24
29
|
export type LocalExtensionSummary = Readonly<{
|
|
25
30
|
id: string;
|
|
26
31
|
revision: string;
|
|
@@ -50,6 +55,8 @@ export type LocalProjectExtensions = Readonly<{
|
|
|
50
55
|
export type LocalAgentExtensions = Readonly<{
|
|
51
56
|
roster: readonly RosteredExtensionV0[];
|
|
52
57
|
grants: readonly ExtensionGrantV0[];
|
|
58
|
+
/** Skills folders of extensions this Agent opted into, read in place. */
|
|
59
|
+
skills: readonly ExtensionSkillFolder[];
|
|
53
60
|
/** Extensions left out of this Agent's roster, and why. */
|
|
54
61
|
diagnostics: readonly Readonly<{
|
|
55
62
|
extensionId: string;
|
|
@@ -57,11 +64,11 @@ export type LocalAgentExtensions = Readonly<{
|
|
|
57
64
|
}>[];
|
|
58
65
|
}>;
|
|
59
66
|
export type LocalExtensionsOptions = Readonly<{
|
|
60
|
-
/** The user home that owns `.fungi/extensions
|
|
61
|
-
home
|
|
67
|
+
/** The selected user home that owns `.fungi/extensions`. */
|
|
68
|
+
home: string;
|
|
62
69
|
}>;
|
|
63
70
|
/** Open the one extension owner for a user home. */
|
|
64
|
-
export declare function localExtensions(options
|
|
71
|
+
export declare function localExtensions(options: LocalExtensionsOptions): {
|
|
65
72
|
root: string;
|
|
66
73
|
/**
|
|
67
74
|
* Install a package with the shipped Bun package manager (lifecycle
|
|
@@ -69,29 +76,68 @@ export declare function localExtensions(options?: LocalExtensionsOptions): {
|
|
|
69
76
|
* is pinned to the commit Bun's lockfile resolved; updating is another
|
|
70
77
|
* explicit `add`. Missing declared artifacts are a refusal.
|
|
71
78
|
*/
|
|
72
|
-
add(source: string): Promise<
|
|
79
|
+
add(source: string): Promise<LocalInstalledExtension>;
|
|
73
80
|
/** Remove the package and every activation that references it. */
|
|
74
81
|
remove(id: string): Promise<boolean>;
|
|
75
82
|
list(): Promise<readonly LocalExtensionSummary[]>;
|
|
76
83
|
/** Enable the current revision's client part and programs for a project. */
|
|
77
84
|
enableProject(projectRoot: string, id: string): Promise<string>;
|
|
78
85
|
disableProject(projectRoot: string, id: string): Promise<boolean>;
|
|
86
|
+
/**
|
|
87
|
+
* The one check that an Agent holds an extension: refuses with
|
|
88
|
+
* `not-held`, naming every id it lacks. Callers needing a whole request
|
|
89
|
+
* judged before any effect call this first; `enableAgent` applies it too.
|
|
90
|
+
*/
|
|
91
|
+
assertHeldByAgent(agentStoreDirectory: string, ids: readonly string[]): Promise<void>;
|
|
79
92
|
/**
|
|
80
93
|
* Grant one Agent store the current revision's declared capabilities.
|
|
81
94
|
* Running work keeps its captured roster; the Agent applies this when it
|
|
82
|
-
* next opens.
|
|
95
|
+
* next opens. A human (the CLI) names no requester. When an Agent asks, it
|
|
96
|
+
* must hold the extension itself.
|
|
83
97
|
*/
|
|
84
|
-
enableAgent(storeDirectory: string, id: string
|
|
98
|
+
enableAgent(storeDirectory: string, id: string, requester?: Readonly<{
|
|
99
|
+
agentStoreDirectory: string;
|
|
100
|
+
}>): Promise<{
|
|
85
101
|
revision: string;
|
|
86
102
|
capabilities: ("state" | "attention" | "settled" | "send" | "timers" | "commands" | "projection" | "run_tools" | "files" | "prompt" | "compaction_handoff")[];
|
|
103
|
+
skills: boolean;
|
|
87
104
|
}>;
|
|
105
|
+
/**
|
|
106
|
+
* Opt one Agent store into the skills the extension ships, without
|
|
107
|
+
* granting any tools. An existing activation keeps its revision and its
|
|
108
|
+
* capabilities; a new one carries none.
|
|
109
|
+
*/
|
|
110
|
+
enableAgentSkills(storeDirectory: string, id: string): Promise<string>;
|
|
111
|
+
/**
|
|
112
|
+
* Turn only the skills off. Tools stay granted; an activation left with
|
|
113
|
+
* neither is removed.
|
|
114
|
+
*/
|
|
115
|
+
disableAgentSkills(storeDirectory: string, id: string): Promise<boolean>;
|
|
88
116
|
disableAgent(storeDirectory: string, id: string): Promise<boolean>;
|
|
89
117
|
/** The project's enabled client entries and program launchers. */
|
|
90
118
|
project(projectRoot: string): Promise<LocalProjectExtensions>;
|
|
91
|
-
/**
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
119
|
+
/** Capture validated records so one reload cannot mix file generations. */
|
|
120
|
+
captureAgent(): Promise<(storeDirectory: string, projectRoot: string) => Promise<Readonly<{
|
|
121
|
+
roster: readonly RosteredExtensionV0[];
|
|
122
|
+
grants: readonly ExtensionGrantV0[];
|
|
123
|
+
/** Skills folders of extensions this Agent opted into, read in place. */
|
|
124
|
+
skills: readonly ExtensionSkillFolder[];
|
|
125
|
+
/** Extensions left out of this Agent's roster, and why. */
|
|
126
|
+
diagnostics: readonly Readonly<{
|
|
127
|
+
extensionId: string;
|
|
128
|
+
error: Error;
|
|
129
|
+
}>[];
|
|
130
|
+
}>>>;
|
|
131
|
+
agent(storeDirectory: string, projectRoot: string): Promise<Readonly<{
|
|
132
|
+
roster: readonly RosteredExtensionV0[];
|
|
133
|
+
grants: readonly ExtensionGrantV0[];
|
|
134
|
+
/** Skills folders of extensions this Agent opted into, read in place. */
|
|
135
|
+
skills: readonly ExtensionSkillFolder[];
|
|
136
|
+
/** Extensions left out of this Agent's roster, and why. */
|
|
137
|
+
diagnostics: readonly Readonly<{
|
|
138
|
+
extensionId: string;
|
|
139
|
+
error: Error;
|
|
140
|
+
}>[];
|
|
141
|
+
}>>;
|
|
96
142
|
};
|
|
97
143
|
export type LocalExtensions = ReturnType<typeof localExtensions>;
|
package/dist/local-extensions.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
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";
|
|
2
|
+
import { access, constants, mkdir, mkdtemp, readFile, realpath, rename, rm, stat, writeFile, } from "node:fs/promises";
|
|
4
3
|
import { join, resolve, sep } from "node:path";
|
|
5
4
|
import { pathToFileURL } from "node:url";
|
|
6
5
|
import { Mycelium } from "@fungi.computer/mycelium";
|
|
@@ -10,12 +9,19 @@ import * as Schema from "effect/Schema";
|
|
|
10
9
|
import lockfile from "proper-lockfile";
|
|
11
10
|
import writeFileAtomic from "write-file-atomic";
|
|
12
11
|
import { z } from "zod";
|
|
12
|
+
import { describeExtensionSkills, } from "./local-skill-source.js";
|
|
13
13
|
/**
|
|
14
14
|
* Installed Local extensions. One owner holds the package bytes under
|
|
15
15
|
* `~/.fungi/extensions`, and the activations its typed operations write. A
|
|
16
16
|
* project's client part and program launchers, and an Agent's tools, are
|
|
17
17
|
* separate activations keyed by canonical path. Nothing under a project or
|
|
18
18
|
* Agent store authorizes anything: a copied folder starts disabled.
|
|
19
|
+
*
|
|
20
|
+
* A package may also ship a `skills` folder. Its skills are read in place
|
|
21
|
+
* from the installed package (nothing is copied or symlinked) and only while
|
|
22
|
+
* the Agent's activation opts in, so disabling or removing the extension ends
|
|
23
|
+
* them. This covers Local installed extensions only; Hub-hosted extensions
|
|
24
|
+
* are not covered.
|
|
19
25
|
*/
|
|
20
26
|
const capabilities = [
|
|
21
27
|
"state",
|
|
@@ -36,6 +42,8 @@ const revisionPattern = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,127}$/u;
|
|
|
36
42
|
const manifestSchema = z.strictObject({
|
|
37
43
|
id: z.string().transform((value) => decodeExtensionId(value)),
|
|
38
44
|
capabilities: z.array(z.enum(capabilities)).max(10).default([]),
|
|
45
|
+
/** Package-relative folder of skills, offered to an Agent that opts in. */
|
|
46
|
+
skills: z.string().startsWith("./").max(1_024).optional(),
|
|
39
47
|
terminals: z
|
|
40
48
|
.array(z.strictObject({
|
|
41
49
|
id: identifier.max(64),
|
|
@@ -77,6 +85,7 @@ const revisionRecord = z.strictObject({
|
|
|
77
85
|
terminals: z.array(terminalRecord),
|
|
78
86
|
host: z.string().nullable(),
|
|
79
87
|
client: z.string().nullable(),
|
|
88
|
+
skills: z.string().nullable(),
|
|
80
89
|
});
|
|
81
90
|
const installedSchema = z.strictObject({
|
|
82
91
|
extensions: z.record(z.string(), z.strictObject({
|
|
@@ -89,6 +98,8 @@ const activationsSchema = z.strictObject({
|
|
|
89
98
|
agents: z.record(z.string(), z.record(z.string(), z.strictObject({
|
|
90
99
|
revision: z.string(),
|
|
91
100
|
capabilities: z.array(z.enum(capabilities)),
|
|
101
|
+
/** The opt-in to the revision's skills folder; separate from tools. */
|
|
102
|
+
skills: z.boolean(),
|
|
92
103
|
}))),
|
|
93
104
|
});
|
|
94
105
|
const lockSchema = z.looseObject({
|
|
@@ -112,6 +123,24 @@ export class LocalExtensionError extends Error {
|
|
|
112
123
|
this.reason = reason;
|
|
113
124
|
}
|
|
114
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* An Agent holds an extension when its store has granted tools, covering every
|
|
128
|
+
* capability of the current revision. Opting into only the extension's skills
|
|
129
|
+
* is not holding it: skills carry no authority to hand its tools on.
|
|
130
|
+
*/
|
|
131
|
+
function assertAgentHolds(held, installed, ids) {
|
|
132
|
+
const missing = ids.filter((id) => {
|
|
133
|
+
const activation = held?.[id];
|
|
134
|
+
if (activation === undefined || activation.capabilities.length === 0)
|
|
135
|
+
return true;
|
|
136
|
+
const entry = installed.extensions[id];
|
|
137
|
+
const needed = (entry === undefined ? undefined : entry.revisions[entry.current])
|
|
138
|
+
?.capabilities ?? [];
|
|
139
|
+
return !needed.every((capability) => activation.capabilities.includes(capability));
|
|
140
|
+
});
|
|
141
|
+
if (missing.length > 0)
|
|
142
|
+
throw new LocalExtensionError("not-held", `An Agent cannot give ${missing.join(", ")} to another Agent: it does not hold ${missing.length === 1 ? "it" : "them"} itself`);
|
|
143
|
+
}
|
|
115
144
|
function failure(cause) {
|
|
116
145
|
return cause instanceof Error ? cause : new Error(String(cause));
|
|
117
146
|
}
|
|
@@ -121,11 +150,11 @@ function absent(cause) {
|
|
|
121
150
|
(cause.code === "ENOENT" || cause.code === "ENOTDIR"));
|
|
122
151
|
}
|
|
123
152
|
/** Open the one extension owner for a user home. */
|
|
124
|
-
export function localExtensions(options
|
|
125
|
-
const root = join(resolve(options.home
|
|
153
|
+
export function localExtensions(options) {
|
|
154
|
+
const root = join(resolve(options.home), ".fungi", "extensions");
|
|
126
155
|
const installedPath = join(root, "installed.json");
|
|
127
156
|
const activationsPath = join(root, "activations.json");
|
|
128
|
-
async function readJson(path, schema, empty) {
|
|
157
|
+
async function readJson(path, schema, empty, remedy) {
|
|
129
158
|
let text;
|
|
130
159
|
try {
|
|
131
160
|
text = await readFile(path, "utf8");
|
|
@@ -135,10 +164,13 @@ export function localExtensions(options = {}) {
|
|
|
135
164
|
return empty;
|
|
136
165
|
throw cause;
|
|
137
166
|
}
|
|
138
|
-
|
|
167
|
+
const parsed = schema.safeParse(JSON.parse(text));
|
|
168
|
+
if (!parsed.success)
|
|
169
|
+
throw new LocalExtensionError("install", `${path} is from an earlier extension format; ${remedy}`, { cause: parsed.error });
|
|
170
|
+
return parsed.data;
|
|
139
171
|
}
|
|
140
|
-
const readInstalled = () => readJson(installedPath, installedSchema, { extensions: {} });
|
|
141
|
-
const readActivations = () => readJson(activationsPath, activationsSchema, { projects: {}, agents: {} });
|
|
172
|
+
const readInstalled = () => readJson(installedPath, installedSchema, { extensions: {} }, "delete it and run `fungi extension add` again");
|
|
173
|
+
const readActivations = () => readJson(activationsPath, activationsSchema, { projects: {}, agents: {} }, "delete it, then re-enable your extensions");
|
|
142
174
|
/** Serialize owner writes across CLI and service processes. */
|
|
143
175
|
async function update(change) {
|
|
144
176
|
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
@@ -195,6 +227,27 @@ export function localExtensions(options = {}) {
|
|
|
195
227
|
}
|
|
196
228
|
return path;
|
|
197
229
|
}
|
|
230
|
+
/** A declared skills folder must be a directory inside the package. */
|
|
231
|
+
async function declaredSkills(directory, declared) {
|
|
232
|
+
if (declared === undefined)
|
|
233
|
+
return null;
|
|
234
|
+
const path = resolve(directory, declared);
|
|
235
|
+
if (!path.startsWith(`${directory}${sep}`))
|
|
236
|
+
throw new LocalExtensionError("manifest", "skills must point inside its package");
|
|
237
|
+
try {
|
|
238
|
+
const real = await realpath(path);
|
|
239
|
+
if (!real.startsWith(`${await realpath(directory)}${sep}`))
|
|
240
|
+
throw new LocalExtensionError("manifest", "skills must point inside its package");
|
|
241
|
+
if (!(await stat(real)).isDirectory())
|
|
242
|
+
throw new LocalExtensionError("artifact", `The package declares skills but ${declared} is not a directory`);
|
|
243
|
+
}
|
|
244
|
+
catch (cause) {
|
|
245
|
+
if (cause instanceof LocalExtensionError)
|
|
246
|
+
throw cause;
|
|
247
|
+
throw new LocalExtensionError("artifact", `The package declares skills but ${declared} is not in the installed package`, { cause });
|
|
248
|
+
}
|
|
249
|
+
return path;
|
|
250
|
+
}
|
|
198
251
|
return {
|
|
199
252
|
root,
|
|
200
253
|
/**
|
|
@@ -246,6 +299,12 @@ export function localExtensions(options = {}) {
|
|
|
246
299
|
const manifest = selected.pkg.fungi;
|
|
247
300
|
const host = await declaredEntry(selected.directory, selected.pkg.exports, "./host");
|
|
248
301
|
const client = await declaredEntry(selected.directory, selected.pkg.exports, "./client");
|
|
302
|
+
const skills = await declaredSkills(selected.directory, manifest.skills);
|
|
303
|
+
const skillNames = skills === null
|
|
304
|
+
? []
|
|
305
|
+
: (await describeExtensionSkills({ extensionId: manifest.id, directory: skills }, AbortSignal.timeout(10_000))).skills.map((skill) => skill.name);
|
|
306
|
+
if (skills !== null && skillNames.length === 0)
|
|
307
|
+
throw new LocalExtensionError("artifact", `${manifest.id} declares skills but ${manifest.skills ?? ""} contains none`);
|
|
249
308
|
if (manifest.capabilities.length > 0 && host === null)
|
|
250
309
|
throw new LocalExtensionError("artifact", `${manifest.id} declares Agent capabilities but its package exports no ./host entry`);
|
|
251
310
|
for (const terminal of manifest.terminals) {
|
|
@@ -282,6 +341,9 @@ export function localExtensions(options = {}) {
|
|
|
282
341
|
client: client === null
|
|
283
342
|
? null
|
|
284
343
|
: join(target, client.slice(staging.length + sep.length)),
|
|
344
|
+
skills: skills === null
|
|
345
|
+
? null
|
|
346
|
+
: join(target, skills.slice(staging.length + sep.length)),
|
|
285
347
|
};
|
|
286
348
|
await update((installed) => {
|
|
287
349
|
const existing = installed.extensions[manifest.id];
|
|
@@ -301,6 +363,7 @@ export function localExtensions(options = {}) {
|
|
|
301
363
|
commit,
|
|
302
364
|
capabilities: record.capabilities,
|
|
303
365
|
terminals: record.terminals.map((terminal) => terminal.id),
|
|
366
|
+
skills: skillNames,
|
|
304
367
|
};
|
|
305
368
|
}
|
|
306
369
|
finally {
|
|
@@ -365,22 +428,86 @@ export function localExtensions(options = {}) {
|
|
|
365
428
|
return true;
|
|
366
429
|
});
|
|
367
430
|
},
|
|
431
|
+
/**
|
|
432
|
+
* The one check that an Agent holds an extension: refuses with
|
|
433
|
+
* `not-held`, naming every id it lacks. Callers needing a whole request
|
|
434
|
+
* judged before any effect call this first; `enableAgent` applies it too.
|
|
435
|
+
*/
|
|
436
|
+
async assertHeldByAgent(agentStoreDirectory, ids) {
|
|
437
|
+
const store = await realpath(agentStoreDirectory);
|
|
438
|
+
const [installed, activations] = await Promise.all([
|
|
439
|
+
readInstalled(),
|
|
440
|
+
readActivations(),
|
|
441
|
+
]);
|
|
442
|
+
assertAgentHolds(activations.agents[store], installed, ids);
|
|
443
|
+
},
|
|
368
444
|
/**
|
|
369
445
|
* Grant one Agent store the current revision's declared capabilities.
|
|
370
446
|
* Running work keeps its captured roster; the Agent applies this when it
|
|
371
|
-
* next opens.
|
|
447
|
+
* next opens. A human (the CLI) names no requester. When an Agent asks, it
|
|
448
|
+
* must hold the extension itself.
|
|
372
449
|
*/
|
|
373
|
-
async enableAgent(storeDirectory, id) {
|
|
450
|
+
async enableAgent(storeDirectory, id, requester) {
|
|
374
451
|
const store = await realpath(storeDirectory);
|
|
452
|
+
const requesterStore = requester === undefined
|
|
453
|
+
? undefined
|
|
454
|
+
: await realpath(requester.agentStoreDirectory);
|
|
375
455
|
return update((installed, activations) => {
|
|
456
|
+
if (requesterStore !== undefined)
|
|
457
|
+
assertAgentHolds(activations.agents[requesterStore], installed, [id]);
|
|
376
458
|
const { revision, record } = currentRevision(installed, id);
|
|
377
459
|
if (record.host === null || record.capabilities.length === 0)
|
|
378
460
|
throw new LocalExtensionError("no-tools", `Extension ${id} declares no Agent capabilities`);
|
|
461
|
+
const skills = activations.agents[store]?.[id]?.skills === true &&
|
|
462
|
+
record.skills !== null;
|
|
463
|
+
activations.agents[store] = {
|
|
464
|
+
...activations.agents[store],
|
|
465
|
+
[id]: { revision, capabilities: record.capabilities, skills },
|
|
466
|
+
};
|
|
467
|
+
return { revision, capabilities: record.capabilities, skills };
|
|
468
|
+
});
|
|
469
|
+
},
|
|
470
|
+
/**
|
|
471
|
+
* Opt one Agent store into the skills the extension ships, without
|
|
472
|
+
* granting any tools. An existing activation keeps its revision and its
|
|
473
|
+
* capabilities; a new one carries none.
|
|
474
|
+
*/
|
|
475
|
+
async enableAgentSkills(storeDirectory, id) {
|
|
476
|
+
const store = await realpath(storeDirectory);
|
|
477
|
+
return update((installed, activations) => {
|
|
478
|
+
const current = currentRevision(installed, id);
|
|
479
|
+
const prior = activations.agents[store]?.[id];
|
|
480
|
+
const revision = prior?.revision ?? current.revision;
|
|
481
|
+
const record = installed.extensions[id]?.revisions[revision];
|
|
482
|
+
if (!record?.skills)
|
|
483
|
+
throw new LocalExtensionError("no-skills", `Extension ${id}@${revision} ships no skills`);
|
|
379
484
|
activations.agents[store] = {
|
|
380
485
|
...activations.agents[store],
|
|
381
|
-
[id]: {
|
|
486
|
+
[id]: {
|
|
487
|
+
revision,
|
|
488
|
+
capabilities: prior?.capabilities ?? [],
|
|
489
|
+
skills: true,
|
|
490
|
+
},
|
|
382
491
|
};
|
|
383
|
-
return
|
|
492
|
+
return revision;
|
|
493
|
+
});
|
|
494
|
+
},
|
|
495
|
+
/**
|
|
496
|
+
* Turn only the skills off. Tools stay granted; an activation left with
|
|
497
|
+
* neither is removed.
|
|
498
|
+
*/
|
|
499
|
+
async disableAgentSkills(storeDirectory, id) {
|
|
500
|
+
const store = await realpath(storeDirectory);
|
|
501
|
+
return update((_installed, activations) => {
|
|
502
|
+
const enabled = activations.agents[store];
|
|
503
|
+
const activation = enabled?.[id];
|
|
504
|
+
if (enabled === undefined || activation?.skills !== true)
|
|
505
|
+
return false;
|
|
506
|
+
if (activation.capabilities.length === 0)
|
|
507
|
+
Reflect.deleteProperty(enabled, id);
|
|
508
|
+
else
|
|
509
|
+
enabled[id] = { ...activation, skills: false };
|
|
510
|
+
return true;
|
|
384
511
|
});
|
|
385
512
|
},
|
|
386
513
|
async disableAgent(storeDirectory, id) {
|
|
@@ -432,53 +559,66 @@ export function localExtensions(options = {}) {
|
|
|
432
559
|
}
|
|
433
560
|
return { clients, terminals, diagnostics };
|
|
434
561
|
},
|
|
435
|
-
/**
|
|
436
|
-
|
|
437
|
-
* Each failure is isolated to its extension and reported.
|
|
438
|
-
*/
|
|
439
|
-
async agent(storeDirectory, projectRoot) {
|
|
440
|
-
const store = await realpath(storeDirectory);
|
|
562
|
+
/** Capture validated records so one reload cannot mix file generations. */
|
|
563
|
+
async captureAgent() {
|
|
441
564
|
const [installed, activations] = await Promise.all([
|
|
442
565
|
readInstalled(),
|
|
443
566
|
readActivations(),
|
|
444
567
|
]);
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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 };
|
|
568
|
+
return (storeDirectory, projectRoot) => loadAgent(installed, activations, storeDirectory, projectRoot);
|
|
569
|
+
},
|
|
570
|
+
async agent(storeDirectory, projectRoot) {
|
|
571
|
+
return loadAgent(await readInstalled(), await readActivations(), storeDirectory, projectRoot);
|
|
482
572
|
},
|
|
483
573
|
};
|
|
574
|
+
async function loadAgent(installed, activations, storeDirectory, projectRoot) {
|
|
575
|
+
const store = await realpath(storeDirectory);
|
|
576
|
+
const roster = [];
|
|
577
|
+
const grants = [];
|
|
578
|
+
const skills = [];
|
|
579
|
+
const diagnostics = [];
|
|
580
|
+
for (const [id, activation] of Object.entries(activations.agents[store] ?? {})) {
|
|
581
|
+
const record = installed.extensions[id]?.revisions[activation.revision];
|
|
582
|
+
try {
|
|
583
|
+
if (record === undefined)
|
|
584
|
+
throw new LocalExtensionError("not-installed", `Extension ${id}@${activation.revision} is enabled but not installed`);
|
|
585
|
+
if (activation.skills && record.skills !== null)
|
|
586
|
+
skills.push({ extensionId: id, directory: record.skills });
|
|
587
|
+
// A skills-only activation grants no tools and loads no host code.
|
|
588
|
+
if (activation.capabilities.length === 0)
|
|
589
|
+
continue;
|
|
590
|
+
if (record.host === null)
|
|
591
|
+
throw new LocalExtensionError("not-installed", `Extension ${id}@${activation.revision} is enabled but its host entry is not installed`);
|
|
592
|
+
const module = hostModuleSchema.parse(await import(pathToFileURL(record.host).href));
|
|
593
|
+
const extension = await module.default({
|
|
594
|
+
runtimeDirectory: await runtimeDirectory(projectRoot, id),
|
|
595
|
+
defineExtension,
|
|
596
|
+
Mycelium,
|
|
597
|
+
z,
|
|
598
|
+
});
|
|
599
|
+
const parsed = rosteredSchema.parse(extension);
|
|
600
|
+
if (parsed.manifest.id !== id)
|
|
601
|
+
throw new LocalExtensionError("load", `Extension ${id} host returned ${parsed.manifest.id}`);
|
|
602
|
+
const requested = new Set(parsed.manifest.requests);
|
|
603
|
+
roster.push(z
|
|
604
|
+
.custom((value) => rosteredSchema.safeParse(value).success)
|
|
605
|
+
.parse(extension));
|
|
606
|
+
grants.push({
|
|
607
|
+
id: decodeExtensionId(id),
|
|
608
|
+
capabilities: activation.capabilities.filter((capability) => requested.has(capability)),
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
catch (cause) {
|
|
612
|
+
diagnostics.push({
|
|
613
|
+
extensionId: id,
|
|
614
|
+
error: cause instanceof LocalExtensionError
|
|
615
|
+
? cause
|
|
616
|
+
: new LocalExtensionError("load", `Extension ${id} could not load`, { cause: failure(cause) }),
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
// Same skill name in two extensions: the lower extension id wins, always.
|
|
621
|
+
skills.sort((a, b) => a.extensionId.localeCompare(b.extensionId));
|
|
622
|
+
return { roster, grants, skills, diagnostics };
|
|
623
|
+
}
|
|
484
624
|
}
|