@agentxm/extension-lifecycle 0.28.4-bootstrap.0 → 0.28.5
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/dist/src/configured-entry-resolution.js +6 -3
- package/dist/src/hooks/manager.js +14 -7
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/knowledge/manager.js +1 -5
- package/dist/src/mcps/manager.js +40 -6
- package/dist/src/mcps/operations/install.d.ts +22 -0
- package/dist/src/mcps/operations/install.js +111 -44
- package/dist/src/mcps/operations/uninstall.js +34 -15
- package/dist/src/rules/manager.js +13 -15
- package/dist/src/subagents/manager.js +48 -3
- package/package.json +10 -8
|
@@ -122,7 +122,9 @@ export const resolveConfiguredRegistryEntry = (name, source, expectedType, relea
|
|
|
122
122
|
detail: `Configured ${expectedType} "${name}" uses a ${pluralType} Registry source`,
|
|
123
123
|
});
|
|
124
124
|
}
|
|
125
|
-
if (
|
|
125
|
+
if (expectedType !== "mcp-server" &&
|
|
126
|
+
parsedPattern?.name !== undefined &&
|
|
127
|
+
parsedPattern.name !== name) {
|
|
126
128
|
return yield* new ExtensionLifecycleFailed({
|
|
127
129
|
category: "validation",
|
|
128
130
|
detail: `Configured ${expectedType} "${name}" points to Registry extension "${parsedPattern.name}"`,
|
|
@@ -136,13 +138,14 @@ export const resolveConfiguredRegistryEntry = (name, source, expectedType, relea
|
|
|
136
138
|
});
|
|
137
139
|
}
|
|
138
140
|
const versionRange = Option.fromUndefinedOr(parsedPattern?.versionRange);
|
|
141
|
+
const registryName = expectedType === "mcp-server" ? (parsedPattern?.name ?? name) : name;
|
|
139
142
|
const workspace = yield* WorkspaceMutations;
|
|
140
143
|
const acceptedRef = yield* acceptedResolutionRef({
|
|
141
144
|
workspace,
|
|
142
145
|
type: expectedType,
|
|
143
146
|
name,
|
|
144
147
|
});
|
|
145
|
-
const accepted = Option.flatMap(acceptedRef, (ref) => ref.refType === "registry" && ref.owner === owner && ref.name ===
|
|
148
|
+
const accepted = Option.flatMap(acceptedRef, (ref) => ref.refType === "registry" && ref.owner === owner && ref.name === registryName
|
|
146
149
|
? Option.some({
|
|
147
150
|
version: ref.version,
|
|
148
151
|
publisherBindingId: ref.publisherBindingId,
|
|
@@ -150,7 +153,7 @@ export const resolveConfiguredRegistryEntry = (name, source, expectedType, relea
|
|
|
150
153
|
: Option.none());
|
|
151
154
|
const providers = yield* SourceHostProviders;
|
|
152
155
|
const resolution = yield* providers.resolveNamedRegistry(resolvedSource, {
|
|
153
|
-
name,
|
|
156
|
+
name: registryName,
|
|
154
157
|
type: expectedType,
|
|
155
158
|
owner,
|
|
156
159
|
versionRange,
|
|
@@ -11,7 +11,7 @@ import * as Option from "effect/Option";
|
|
|
11
11
|
import * as Path from "effect/Path";
|
|
12
12
|
import * as Schema from "effect/Schema";
|
|
13
13
|
import * as Semaphore from "effect/Semaphore";
|
|
14
|
-
import { HookConfigInvalid, HookDefinitionInvalid, HookInstallStateMissing, HookIoFailed, activeContributors, applyProjectionPlans, planAggregateProjection, runWithTransientFileBackup, reconcileManagedRegionFile, managedHookCommands, readManagedHookCommands, updateHooksJson, evaluateHookAgentOutcome, WriteBackupRetained, resolveInstructionsConfig, } from "@agentxm/extension-workspace";
|
|
14
|
+
import { HookConfigInvalid, HookDefinitionInvalid, HookInstallStateMissing, HookIoFailed, activeContributors, applyProjectionPlans, planAggregateProjection, runWithTransientFileBackup, reconcileManagedRegionFile, projectionGeneration, managedHookCommands, readManagedHookCommands, updateHooksJson, evaluateHookAgentOutcome, WriteBackupRetained, resolveInstructionsConfig, } from "@agentxm/extension-workspace";
|
|
15
15
|
import { AGENTS as CAPABILITY_AGENTS, installable, } from "@agentxm/extension-model/unstable/agent-capabilities";
|
|
16
16
|
import { computePackageContentHash } from "@agentxm/workspace-state";
|
|
17
17
|
import { computeMaterializedTreeIntegrity } from "@agentxm/workspace-state";
|
|
@@ -437,12 +437,25 @@ export const HookManagerLive = Layer.effect(HookManager, Effect.gen(function* ()
|
|
|
437
437
|
const rendered = input.contributors
|
|
438
438
|
.map((hook) => `### ${hook.manifest.title ?? hook.name}\n\nFor agents without a usable native hook mapping (${hook.fallbackAgentIds.join(", ")}), treat this as a managed advisory rule. After the matching lifecycle event (${hook.manifest.bindings.map((binding) => binding.on).join(", ")}), run \`${hook.command}\` and address any findings before continuing.`)
|
|
439
439
|
.join("\n\n");
|
|
440
|
+
const generation = projectionGeneration([
|
|
441
|
+
"hook-fallback-region-v1",
|
|
442
|
+
target.workspaceRelative,
|
|
443
|
+
HOOK_FALLBACKS_REGION_OWNER,
|
|
444
|
+
...input.contributors.flatMap((contributor) => [
|
|
445
|
+
contributor.name,
|
|
446
|
+
contributor.marker,
|
|
447
|
+
contributor.command,
|
|
448
|
+
JSON.stringify(contributor.fallbackAgentIds),
|
|
449
|
+
JSON.stringify(contributor.manifest),
|
|
450
|
+
]),
|
|
451
|
+
]);
|
|
440
452
|
const { changed, observedRegion } = yield* provide(reconcileManagedRegionFile({
|
|
441
453
|
targetPath: target.targetPath,
|
|
442
454
|
displayPath: target.workspaceRelative,
|
|
443
455
|
region: HOOK_FALLBACKS_REGION,
|
|
444
456
|
owner: HOOK_FALLBACKS_REGION_OWNER,
|
|
445
457
|
rendered,
|
|
458
|
+
generation,
|
|
446
459
|
...(options?.dryRun === undefined ? {} : { dryRun: options.dryRun }),
|
|
447
460
|
unsupportedTargetDetail: `Hook fallback target does not support managed regions: ${target.workspaceRelative}`,
|
|
448
461
|
}));
|
|
@@ -464,12 +477,6 @@ export const HookManagerLive = Layer.effect(HookManager, Effect.gen(function* ()
|
|
|
464
477
|
present: Option.isSome(observedRegion),
|
|
465
478
|
current: !changed,
|
|
466
479
|
expectedContributors: input.contributors.map(({ marker }) => marker),
|
|
467
|
-
observedContributors: Option.match(observedRegion, {
|
|
468
|
-
onNone: () => [],
|
|
469
|
-
onSome: (region) => input.contributors
|
|
470
|
-
.filter(({ command }) => region.includes(command))
|
|
471
|
-
.map(({ marker }) => marker),
|
|
472
|
-
}),
|
|
473
480
|
},
|
|
474
481
|
};
|
|
475
482
|
});
|
package/dist/src/index.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export type { InstallResult } from "./skills/operations/install-result.js";
|
|
|
23
23
|
export { uninstallSkill, type UninstallSkillOperation, type UninstallSkillOperationArgs, } from "./skills/operations/uninstall.js";
|
|
24
24
|
export { enableSkill, type EnableSkillOperation } from "./skills/operations/enable.js";
|
|
25
25
|
export { disableSkill, type DisableSkillOperation } from "./skills/operations/disable.js";
|
|
26
|
-
export { installMcpServer, type InstallMcpServerOperation, type InstallMcpServerOperationArgs, } from "./mcps/operations/install.js";
|
|
26
|
+
export { collectSecretInputNames, deleteMcpSecrets, installMcpServer, mcpSecretAccount, readMcpServerManifest, type InstallMcpServerOperation, type InstallMcpServerOperationArgs, } from "./mcps/operations/install.js";
|
|
27
27
|
export { uninstallMcpServer, type UninstallMcpServerOperation, type UninstallMcpServerOperationArgs, } from "./mcps/operations/uninstall.js";
|
|
28
28
|
export { enableMcpServer, type EnableMcpServerOperation } from "./mcps/operations/enable.js";
|
|
29
29
|
export { disableMcpServer, type DisableMcpServerOperation } from "./mcps/operations/disable.js";
|
package/dist/src/index.js
CHANGED
|
@@ -24,7 +24,7 @@ export { uninstallSkill, } from "./skills/operations/uninstall.js";
|
|
|
24
24
|
export { enableSkill } from "./skills/operations/enable.js";
|
|
25
25
|
export { disableSkill } from "./skills/operations/disable.js";
|
|
26
26
|
// MCP server lifecycle operations
|
|
27
|
-
export { installMcpServer, } from "./mcps/operations/install.js";
|
|
27
|
+
export { collectSecretInputNames, deleteMcpSecrets, installMcpServer, mcpSecretAccount, readMcpServerManifest, } from "./mcps/operations/install.js";
|
|
28
28
|
export { uninstallMcpServer, } from "./mcps/operations/uninstall.js";
|
|
29
29
|
export { enableMcpServer } from "./mcps/operations/enable.js";
|
|
30
30
|
export { disableMcpServer } from "./mcps/operations/disable.js";
|
|
@@ -11,7 +11,7 @@ import * as Schema from "effect/Schema";
|
|
|
11
11
|
import * as Scope from "effect/Scope";
|
|
12
12
|
import * as Result from "effect/Result";
|
|
13
13
|
import { PlatformError } from "effect/PlatformError";
|
|
14
|
-
import { KnowledgeDefinitionInvalid, KnowledgeDesiredStateUnreconcilable, KnowledgeInstallStateMissing, KnowledgeIoFailed, KnowledgeObservableContractViolated, KnowledgeResolutionMissing, KnowledgeUnavailable, applyProjectionPlans, planAggregateProjection, requireCompleteGraph, KNOWLEDGE_REGION_OWNER, reconcileKnowledgeDiscovery,
|
|
14
|
+
import { KnowledgeDefinitionInvalid, KnowledgeDesiredStateUnreconcilable, KnowledgeInstallStateMissing, KnowledgeIoFailed, KnowledgeObservableContractViolated, KnowledgeResolutionMissing, KnowledgeUnavailable, applyProjectionPlans, planAggregateProjection, requireCompleteGraph, KNOWLEDGE_REGION_OWNER, reconcileKnowledgeDiscovery, resolveInstructionsConfig, } from "@agentxm/extension-workspace";
|
|
15
15
|
import { canReuseInstalledPackage, materializeExternalPackage } from "@agentxm/extension-workspace";
|
|
16
16
|
import { materializeRegistryPackage } from "../registry-materialization.js";
|
|
17
17
|
import { computeExtensionPathsForLayout, extensionPathSourceFromLockEntry, } from "@agentxm/workspace-state";
|
|
@@ -351,10 +351,6 @@ export const KnowledgeManagerLive = Layer.effect(KnowledgeManager, Effect.gen(fu
|
|
|
351
351
|
present: Option.isSome(result.observedRegion),
|
|
352
352
|
current: !result.changed,
|
|
353
353
|
expectedContributors: input.contributors.map(({ owner, name }) => `${owner}/knowledge/${name}`),
|
|
354
|
-
observedContributors: Option.match(result.observedRegion, {
|
|
355
|
-
onNone: () => [],
|
|
356
|
-
onSome: observedKnowledgeContributors,
|
|
357
|
-
}),
|
|
358
354
|
}))),
|
|
359
355
|
apply: (input) => runKnowledgeProjectionAdapter({
|
|
360
356
|
bundles: input.contributors,
|
package/dist/src/mcps/manager.js
CHANGED
|
@@ -15,7 +15,7 @@ import * as Option from "effect/Option";
|
|
|
15
15
|
import { McpConfigIoFailed, McpInstallStateMissing, McpRegistryOnlyInstall, removeMcpServerFromManifest, applyProjectionPlans, planSingletonProjection, } from "@agentxm/extension-workspace";
|
|
16
16
|
import { McpServerManager } from "@agentxm/extension-workspace";
|
|
17
17
|
import { configuredMcpServersToDiskRefs } from "@agentxm/extension-workspace";
|
|
18
|
-
import { WorkspaceMutations } from "@agentxm/workspace-state";
|
|
18
|
+
import { mcpRegistryResolutionKey, WorkspaceMutations } from "@agentxm/workspace-state";
|
|
19
19
|
import { canReuseInstalledPackage } from "@agentxm/extension-workspace";
|
|
20
20
|
import { materializeRegistryPackageWithTreeIntegrity } from "../registry-materialization.js";
|
|
21
21
|
import { computeExtensionPathsForLayout } from "@agentxm/workspace-state";
|
|
@@ -55,6 +55,7 @@ export const McpServerManagerLive = Layer.effect(McpServerManager, Effect.gen(fu
|
|
|
55
55
|
const fsPathLayer = Layer.mergeAll(Layer.succeed(FileSystem.FileSystem, fs), Layer.succeed(Path.Path, path), Layer.succeed(HttpClient.HttpClient, httpClient));
|
|
56
56
|
const provide = (effect) => Effect.provide(effect, fsPathLayer);
|
|
57
57
|
const lastTreeIntegrities = new Map();
|
|
58
|
+
const pendingRemoval = new Map();
|
|
58
59
|
const materializeInstall = Effect.fn("McpServerManager.materializeInstall")(function* ({ ref, force }) {
|
|
59
60
|
if (ref.refType !== "registry") {
|
|
60
61
|
return yield* new McpRegistryOnlyInstall({
|
|
@@ -64,7 +65,11 @@ export const McpServerManagerLive = Layer.effect(McpServerManager, Effect.gen(fu
|
|
|
64
65
|
}
|
|
65
66
|
const registryRef = ref;
|
|
66
67
|
const canonicalPath = computeExtensionPathsForLayout(path.join, ws.layout, registryRef, "mcps", registryRef.name).canonicalPath;
|
|
67
|
-
const lockedEntry = yield* ws.getLockedMcpServer(
|
|
68
|
+
const lockedEntry = yield* ws.getLockedMcpServer(mcpRegistryResolutionKey({
|
|
69
|
+
authority: registryRef.source.location,
|
|
70
|
+
owner: registryRef.owner,
|
|
71
|
+
name: registryRef.server.name,
|
|
72
|
+
}));
|
|
68
73
|
const lockedVersion = acceptedRegistryVersionForRef(lockedEntry, registryRef);
|
|
69
74
|
const useExisting = yield* provide(canReuseInstalledPackage({
|
|
70
75
|
installedPath: canonicalPath,
|
|
@@ -97,6 +102,18 @@ export const McpServerManagerLive = Layer.effect(McpServerManager, Effect.gen(fu
|
|
|
97
102
|
lastTreeIntegrities.set(registryRef.server.name, materialized.treeIntegrity);
|
|
98
103
|
}, Effect.asVoid);
|
|
99
104
|
const makeMaterializeRemoval = (retainCanonical) => Effect.fn("McpServerManager.materializeRemoval")(function* ({ target }) {
|
|
105
|
+
const graph = yield* ws.getDesiredStateGraph();
|
|
106
|
+
const desiredNode = graph.nodes.find((node) => node.type === "mcp-server" && node.name === target.name);
|
|
107
|
+
const closure = desiredNode === undefined || desiredNode.authority === "inline"
|
|
108
|
+
? undefined
|
|
109
|
+
: graph.mcpSourceClosures.find((candidate) => candidate.identity === desiredNode.identity);
|
|
110
|
+
const retainShared = closure !== undefined && closure.localNames.some((name) => name !== target.name);
|
|
111
|
+
pendingRemoval.set(target.name, {
|
|
112
|
+
...(desiredNode === undefined || desiredNode.authority === "inline"
|
|
113
|
+
? {}
|
|
114
|
+
: { resolutionKey: desiredNode.identity }),
|
|
115
|
+
retainShared,
|
|
116
|
+
});
|
|
100
117
|
const configuredAgents = yield* ws.getConfiguredAgents();
|
|
101
118
|
yield* applyProjectionPlans(configuredAgents.map((agentId) => planSingletonProjection({
|
|
102
119
|
unitId: "mcp-server:native-config-entry",
|
|
@@ -119,7 +136,7 @@ export const McpServerManagerLive = Layer.effect(McpServerManager, Effect.gen(fu
|
|
|
119
136
|
})).pipe(Effect.asVoid),
|
|
120
137
|
},
|
|
121
138
|
})));
|
|
122
|
-
if (retainCanonical)
|
|
139
|
+
if (retainCanonical || retainShared)
|
|
123
140
|
return;
|
|
124
141
|
const canonical = yield* provide(acceptedCanonicalObservation({
|
|
125
142
|
workspace: ws,
|
|
@@ -178,6 +195,11 @@ export const McpServerManagerLive = Layer.effect(McpServerManager, Effect.gen(fu
|
|
|
178
195
|
const lockEntry = buildMcpServerLockEntry(registryRef, treeIntegrity);
|
|
179
196
|
return ws.setMcpServer({
|
|
180
197
|
name: ref.server.name,
|
|
198
|
+
resolutionKey: mcpRegistryResolutionKey({
|
|
199
|
+
authority: registryRef.source.location,
|
|
200
|
+
owner: registryRef.owner,
|
|
201
|
+
name: registryRef.server.name,
|
|
202
|
+
}),
|
|
181
203
|
lockEntry,
|
|
182
204
|
versionRange,
|
|
183
205
|
});
|
|
@@ -200,14 +222,26 @@ export const McpServerManagerLive = Layer.effect(McpServerManager, Effect.gen(fu
|
|
|
200
222
|
const lockEntry = buildMcpServerLockEntry(registryRef, treeIntegrity);
|
|
201
223
|
return ws.setMcpServerLock({
|
|
202
224
|
name: ref.server.name,
|
|
225
|
+
resolutionKey: mcpRegistryResolutionKey({
|
|
226
|
+
authority: registryRef.source.location,
|
|
227
|
+
owner: registryRef.owner,
|
|
228
|
+
name: registryRef.server.name,
|
|
229
|
+
}),
|
|
203
230
|
lockEntry,
|
|
204
231
|
versionRange: Option.none(),
|
|
205
232
|
});
|
|
206
233
|
}), Effect.withSpan("McpServerManager.upsertLockfileEntry"));
|
|
207
234
|
},
|
|
208
|
-
removeLockfileEntry: ({ target }) =>
|
|
209
|
-
.
|
|
210
|
-
.
|
|
235
|
+
removeLockfileEntry: ({ target }) => {
|
|
236
|
+
const removal = pendingRemoval.get(target.name);
|
|
237
|
+
pendingRemoval.delete(target.name);
|
|
238
|
+
if (removal?.retainShared === true || removal?.resolutionKey === undefined) {
|
|
239
|
+
return Effect.void.pipe(Effect.withSpan("McpServerManager.removeLockfileEntry"));
|
|
240
|
+
}
|
|
241
|
+
return ws
|
|
242
|
+
.removeMcpServerLock(removal.resolutionKey)
|
|
243
|
+
.pipe(Effect.withSpan("McpServerManager.removeLockfileEntry"));
|
|
244
|
+
},
|
|
211
245
|
};
|
|
212
246
|
}));
|
|
213
247
|
//# sourceMappingURL=manager.js.map
|
|
@@ -17,12 +17,15 @@ import type { StepFailure } from "@agentxm/workspace-operations";
|
|
|
17
17
|
import type { JobStepResult, Operation } from "@agentxm/workspace-operations";
|
|
18
18
|
import { WorkspaceMutations } from "@agentxm/workspace-state";
|
|
19
19
|
import type { McpServerExtensionRef } from "@agentxm/extension-model/unstable/extensions/refs/mcp-server";
|
|
20
|
+
import { type McpServerManifest } from "@agentxm/extension-model/unstable/mcps/manifest-schema";
|
|
20
21
|
import { LifecycleFailureAdapter } from "../../failure-adapter.js";
|
|
21
22
|
/**
|
|
22
23
|
* Args for the install-mcp-server operation.
|
|
23
24
|
*/
|
|
24
25
|
export type InstallMcpServerOperationArgs = {
|
|
25
26
|
readonly ref: McpServerExtensionRef;
|
|
27
|
+
/** Local connection identity and exact agent-native MCP key. */
|
|
28
|
+
readonly localName?: string;
|
|
26
29
|
readonly force: boolean;
|
|
27
30
|
/** Explicitly permit a workspace-authored relocation during reconciliation. */
|
|
28
31
|
readonly allowWorkspaceSourceTransition?: boolean;
|
|
@@ -49,6 +52,25 @@ export type InstallMcpServerOperationArgs = {
|
|
|
49
52
|
* @experimental This API is unstable and may change without notice.
|
|
50
53
|
*/
|
|
51
54
|
export type InstallMcpServerOperation = Operation<"install-mcp-server", InstallMcpServerOperationArgs>;
|
|
55
|
+
export declare const mcpSecretAccount: (args: {
|
|
56
|
+
readonly scopeRoot: string;
|
|
57
|
+
readonly localName: string;
|
|
58
|
+
readonly sourceIdentity: string;
|
|
59
|
+
readonly inputName: string;
|
|
60
|
+
}) => string;
|
|
61
|
+
export type McpSecretDeletionOutcome = {
|
|
62
|
+
readonly _tag: "deleted";
|
|
63
|
+
readonly inputName: string;
|
|
64
|
+
} | {
|
|
65
|
+
readonly _tag: "absent";
|
|
66
|
+
readonly inputName: string;
|
|
67
|
+
} | {
|
|
68
|
+
readonly _tag: "failed";
|
|
69
|
+
readonly inputName: string;
|
|
70
|
+
};
|
|
71
|
+
export declare const collectSecretInputNames: (manifest: McpServerManifest) => ReadonlySet<string>;
|
|
72
|
+
export declare const readMcpServerManifest: (canonicalPath: string) => Effect.Effect<Option.Option<McpServerManifest>, never, FileSystem.FileSystem | Path.Path>;
|
|
73
|
+
export declare const deleteMcpSecrets: (identity: Omit<Parameters<typeof mcpSecretAccount>[0], "inputName">, secretNames: ReadonlySet<string>) => Effect.Effect<ReadonlyArray<McpSecretDeletionOutcome>>;
|
|
52
74
|
/**
|
|
53
75
|
* Install-mcp-server operation handler.
|
|
54
76
|
*
|
|
@@ -20,8 +20,9 @@ import * as Array from "effect/Array";
|
|
|
20
20
|
import * as Effect from "effect/Effect";
|
|
21
21
|
import * as Option from "effect/Option";
|
|
22
22
|
import * as Schema from "effect/Schema";
|
|
23
|
+
import { createHash } from "node:crypto";
|
|
23
24
|
import { CodingAgentRepository, McpSharedTargetConflict, applyProjectionPlansWithResults, planSingletonProjection, inspectAgentMcpServer, sharedMcpTargetPolicyConflict, } from "@agentxm/extension-workspace";
|
|
24
|
-
import { isPathSafe } from "@agentxm/workspace-state";
|
|
25
|
+
import { isPathSafe, mcpRegistryResolutionKey } from "@agentxm/workspace-state";
|
|
25
26
|
import { acceptedRegistryVersionForRef, validateExactResolvedVersion, } from "@agentxm/workspace-state";
|
|
26
27
|
import { appendWarningsToMessage } from "@agentxm/workspace-operations";
|
|
27
28
|
import { WorkspaceMutations } from "@agentxm/workspace-state";
|
|
@@ -55,7 +56,9 @@ const buildLockEntry = (ref, treeIntegrity) => ({
|
|
|
55
56
|
treeIntegrity,
|
|
56
57
|
});
|
|
57
58
|
const MCP_SECRET_SERVICE = "axm-mcp";
|
|
58
|
-
const mcpSecretAccount = (
|
|
59
|
+
export const mcpSecretAccount = (args) => createHash("sha256")
|
|
60
|
+
.update([args.scopeRoot, args.localName, args.sourceIdentity, args.inputName].join("\0"))
|
|
61
|
+
.digest("hex");
|
|
59
62
|
const keyringModuleSpecifier = ["@napi-rs", "keyring"].join("/");
|
|
60
63
|
const loadKeyringEntry = Effect.tryPromise({
|
|
61
64
|
try: async () => {
|
|
@@ -64,22 +67,22 @@ const loadKeyringEntry = Effect.tryPromise({
|
|
|
64
67
|
},
|
|
65
68
|
catch: () => undefined,
|
|
66
69
|
});
|
|
67
|
-
const saveMcpSecret = (
|
|
70
|
+
const saveMcpSecret = (account, inputName, value) => Effect.gen(function* () {
|
|
68
71
|
const Entry = yield* loadKeyringEntry;
|
|
69
72
|
return yield* Effect.try({
|
|
70
73
|
try: () => {
|
|
71
|
-
const entry = new Entry(MCP_SECRET_SERVICE,
|
|
74
|
+
const entry = new Entry(MCP_SECRET_SERVICE, account);
|
|
72
75
|
entry.setPassword(value);
|
|
73
76
|
return { _tag: "saved", inputName };
|
|
74
77
|
},
|
|
75
78
|
catch: () => undefined,
|
|
76
79
|
});
|
|
77
80
|
}).pipe(Effect.catch(() => Effect.succeed({ _tag: "failed", inputName })));
|
|
78
|
-
const loadMcpSecret = (
|
|
81
|
+
const loadMcpSecret = (account) => Effect.gen(function* () {
|
|
79
82
|
const Entry = yield* loadKeyringEntry;
|
|
80
83
|
return yield* Effect.try({
|
|
81
84
|
try: () => {
|
|
82
|
-
const entry = new Entry(MCP_SECRET_SERVICE,
|
|
85
|
+
const entry = new Entry(MCP_SECRET_SERVICE, account);
|
|
83
86
|
return Option.fromNullOr(entry.getPassword());
|
|
84
87
|
},
|
|
85
88
|
catch: () => undefined,
|
|
@@ -116,7 +119,7 @@ const collectRequiredInputNames = (manifest) => {
|
|
|
116
119
|
}
|
|
117
120
|
return names;
|
|
118
121
|
};
|
|
119
|
-
const collectSecretInputNames = (manifest) => {
|
|
122
|
+
export const collectSecretInputNames = (manifest) => {
|
|
120
123
|
const names = new Set();
|
|
121
124
|
const add = (input) => {
|
|
122
125
|
const name = maybeSecretInputName(input);
|
|
@@ -179,7 +182,7 @@ const installFromRegistry = (ref, reuse) => Effect.gen(function* () {
|
|
|
179
182
|
}
|
|
180
183
|
return canonicalPath;
|
|
181
184
|
});
|
|
182
|
-
const
|
|
185
|
+
export const readMcpServerManifest = (canonicalPath) => Effect.gen(function* () {
|
|
183
186
|
const fs = yield* FileSystem.FileSystem;
|
|
184
187
|
const path = yield* Path.Path;
|
|
185
188
|
const manifestPath = path.join(canonicalPath, MCP_SERVER_MANIFEST_FILENAME);
|
|
@@ -204,8 +207,8 @@ const isNothingRunnableManifest = (manifest) => Option.match(manifest, {
|
|
|
204
207
|
onSome: (value) => (value.server.packages === undefined || value.server.packages.length === 0) &&
|
|
205
208
|
(value.server.remotes === undefined || value.server.remotes.length === 0),
|
|
206
209
|
});
|
|
207
|
-
const loadStoredMcpSecrets = (
|
|
208
|
-
const entries = yield* Effect.forEach(secretNames, (name) => loadMcpSecret(
|
|
210
|
+
const loadStoredMcpSecrets = (identity, secretNames) => Effect.gen(function* () {
|
|
211
|
+
const entries = yield* Effect.forEach(secretNames, (name) => loadMcpSecret(mcpSecretAccount({ ...identity, inputName: name })).pipe(Effect.map((value) => ({ name, value }))), { concurrency: "unbounded" });
|
|
209
212
|
const loaded = {};
|
|
210
213
|
for (const { name, value } of entries) {
|
|
211
214
|
if (Option.isSome(value))
|
|
@@ -213,12 +216,24 @@ const loadStoredMcpSecrets = (serverName, secretNames) => Effect.gen(function* (
|
|
|
213
216
|
}
|
|
214
217
|
return loaded;
|
|
215
218
|
});
|
|
216
|
-
const persistMcpSecrets = (
|
|
219
|
+
const persistMcpSecrets = (identity, secretNames, values) => Effect.forEach(secretNames, (name) => {
|
|
217
220
|
const value = values[name];
|
|
218
221
|
return value === undefined
|
|
219
222
|
? Effect.succeed({ _tag: "skipped", inputName: name })
|
|
220
|
-
: saveMcpSecret(
|
|
223
|
+
: saveMcpSecret(mcpSecretAccount({ ...identity, inputName: name }), name, value);
|
|
221
224
|
}, { concurrency: "unbounded" });
|
|
225
|
+
export const deleteMcpSecrets = (identity, secretNames) => Effect.forEach(secretNames, (inputName) => Effect.gen(function* () {
|
|
226
|
+
const Entry = yield* loadKeyringEntry;
|
|
227
|
+
return yield* Effect.try({
|
|
228
|
+
try: () => {
|
|
229
|
+
const entry = new Entry(MCP_SECRET_SERVICE, mcpSecretAccount({ ...identity, inputName }));
|
|
230
|
+
return entry.deletePassword()
|
|
231
|
+
? { _tag: "deleted", inputName }
|
|
232
|
+
: { _tag: "absent", inputName };
|
|
233
|
+
},
|
|
234
|
+
catch: () => undefined,
|
|
235
|
+
});
|
|
236
|
+
}).pipe(Effect.catch(() => Effect.succeed({ _tag: "failed", inputName }))), { concurrency: "unbounded" });
|
|
222
237
|
const redactSettingsEnv = (values, secretNames) => {
|
|
223
238
|
const redacted = {};
|
|
224
239
|
for (const [name, value] of Object.entries(values)) {
|
|
@@ -398,9 +413,10 @@ const syncConfiguredAgentsOnInstall = (args) => Effect.gen(function* () {
|
|
|
398
413
|
* then update lockfile/settings.
|
|
399
414
|
*/
|
|
400
415
|
export const installMcpServer = (op) => Effect.gen(function* () {
|
|
401
|
-
const adapter = yield* LifecycleFailureAdapter;
|
|
402
416
|
const ws = yield* WorkspaceMutations;
|
|
417
|
+
const path = yield* Path.Path;
|
|
403
418
|
const { ref } = op.args;
|
|
419
|
+
const localName = op.args.localName ?? ref.server.name;
|
|
404
420
|
if (ref.refType !== "registry" && ref.refType !== "workspace") {
|
|
405
421
|
return yield* new ExtensionLifecycleFailed({
|
|
406
422
|
category: "usage",
|
|
@@ -415,8 +431,26 @@ export const installMcpServer = (op) => Effect.gen(function* () {
|
|
|
415
431
|
}
|
|
416
432
|
const strictAgentSync = Option.getOrElse(op.args.strictAgentSync ?? Option.none(), () => false);
|
|
417
433
|
const env = Option.getOrElse(op.args.env ?? Option.none(), () => ({}));
|
|
434
|
+
const resolutionKey = ref.refType === "registry"
|
|
435
|
+
? mcpRegistryResolutionKey({
|
|
436
|
+
authority: ref.source.location,
|
|
437
|
+
owner: ref.owner,
|
|
438
|
+
name: ref.server.name,
|
|
439
|
+
})
|
|
440
|
+
: undefined;
|
|
441
|
+
const sourceIdentity = resolutionKey ?? `workspace:${ref.owner}/mcps/${ref.server.name}`;
|
|
442
|
+
const desiredGraph = yield* ws.getDesiredStateGraph();
|
|
443
|
+
const existingLocalNode = desiredGraph.nodes.find((node) => node.type === "mcp-server" && node.name === localName);
|
|
444
|
+
if (existingLocalNode !== undefined &&
|
|
445
|
+
(existingLocalNode.authority === "inline" || existingLocalNode.identity !== sourceIdentity)) {
|
|
446
|
+
return yield* new ExtensionLifecycleFailed({
|
|
447
|
+
category: "conflict",
|
|
448
|
+
detail: `Local MCP name "${localName}" is already owned by a different source`,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
const existingClosure = desiredGraph.mcpSourceClosures.find((closure) => closure.identity === sourceIdentity);
|
|
418
452
|
const lockedVersion = ref.refType === "registry"
|
|
419
|
-
? acceptedRegistryVersionForRef(yield* ws.getLockedMcpServer(
|
|
453
|
+
? acceptedRegistryVersionForRef(yield* ws.getLockedMcpServer(resolutionKey ?? ""), ref)
|
|
420
454
|
: undefined;
|
|
421
455
|
const canonicalPath = ref.refType === "registry"
|
|
422
456
|
? yield* installFromRegistry(ref, { force: op.args.force, lockedVersion })
|
|
@@ -446,7 +480,7 @@ export const installMcpServer = (op) => Effect.gen(function* () {
|
|
|
446
480
|
}
|
|
447
481
|
return ref.location;
|
|
448
482
|
});
|
|
449
|
-
const manifest = yield*
|
|
483
|
+
const manifest = yield* readMcpServerManifest(canonicalPath);
|
|
450
484
|
const nothingRunnable = isNothingRunnableManifest(manifest);
|
|
451
485
|
const secretNames = Option.match(manifest, {
|
|
452
486
|
onNone: () => new Set(),
|
|
@@ -459,8 +493,13 @@ export const installMcpServer = (op) => Effect.gen(function* () {
|
|
|
459
493
|
? buildLockEntry(ref, yield* computeMaterializedTreeIntegrity(canonicalPath))
|
|
460
494
|
: undefined;
|
|
461
495
|
const currentMcpServers = yield* ws.getConfiguredMcpServerEntries();
|
|
462
|
-
const currentEntry = currentMcpServers[
|
|
463
|
-
const
|
|
496
|
+
const currentEntry = currentMcpServers[localName];
|
|
497
|
+
const secretIdentity = {
|
|
498
|
+
scopeRoot: path.resolve(ws.baseDir),
|
|
499
|
+
localName,
|
|
500
|
+
sourceIdentity,
|
|
501
|
+
};
|
|
502
|
+
const storedSecrets = yield* loadStoredMcpSecrets(secretIdentity, secretNames);
|
|
464
503
|
const mergedEnv = { ...storedSecrets, ...(currentEntry?.env ?? {}), ...env };
|
|
465
504
|
// Under --non-interactive there is nobody to prompt, so a required input
|
|
466
505
|
// that nothing supplied would otherwise install a server that cannot start.
|
|
@@ -475,7 +514,7 @@ export const installMcpServer = (op) => Effect.gen(function* () {
|
|
|
475
514
|
if (missingInputs.length > 0 && op.args.nonInteractive) {
|
|
476
515
|
return yield* new ExtensionLifecycleFailed({
|
|
477
516
|
category: "usage",
|
|
478
|
-
detail: `${
|
|
517
|
+
detail: `${localName} needs ${missingInputs.join(", ")}, and --non-interactive cannot prompt for them`,
|
|
479
518
|
suggestions: [
|
|
480
519
|
{
|
|
481
520
|
description: "Supply each required input on the command line",
|
|
@@ -494,50 +533,78 @@ export const installMcpServer = (op) => Effect.gen(function* () {
|
|
|
494
533
|
enabled,
|
|
495
534
|
...(agents === undefined ? {} : { agents }),
|
|
496
535
|
};
|
|
497
|
-
const agentSync = yield* syncConfiguredAgentsOnInstall({
|
|
498
|
-
wsBaseDir: ws.baseDir,
|
|
499
|
-
scope: ws.scope,
|
|
500
|
-
strict: strictAgentSync,
|
|
501
|
-
serverName: ref.server.name,
|
|
502
|
-
canonicalPath,
|
|
503
|
-
owner: ref.owner,
|
|
504
|
-
resolvedVersion: ref.version,
|
|
505
|
-
nothingRunnable,
|
|
506
|
-
enabled,
|
|
507
|
-
configValues: preserveSecretReferences(mergedEnv, secretNames),
|
|
508
|
-
entry: settingsEntry,
|
|
509
|
-
});
|
|
510
|
-
const secretPersistence = yield* persistMcpSecrets(ref.server.name, secretNames, mergedEnv);
|
|
511
|
-
const secretWarnings = secretPersistence.flatMap((outcome) => outcome._tag === "failed"
|
|
512
|
-
? [`${outcome.inputName} could not be saved to the system keychain`]
|
|
513
|
-
: []);
|
|
514
536
|
const writeEffect = op.args.skipStateWrites === true
|
|
515
537
|
? Effect.void
|
|
516
538
|
: Option.getOrElse(op.args.skipSettings, () => false)
|
|
517
539
|
? lockEntry === undefined
|
|
518
540
|
? Effect.void
|
|
519
541
|
: ws.setMcpServerLock({
|
|
520
|
-
name: ref.server.name,
|
|
542
|
+
name: resolutionKey ?? ref.server.name,
|
|
543
|
+
resolutionKey: resolutionKey ?? ref.server.name,
|
|
521
544
|
lockEntry,
|
|
522
545
|
versionRange: Option.none(),
|
|
523
546
|
})
|
|
524
547
|
: lockEntry === undefined
|
|
525
|
-
? ws.setMcpServerEntry(
|
|
548
|
+
? ws.setMcpServerEntry(localName, {
|
|
526
549
|
...settingsEntry,
|
|
527
550
|
})
|
|
528
551
|
: ws.setMcpServer({
|
|
529
|
-
name:
|
|
552
|
+
name: localName,
|
|
553
|
+
resolutionKey: resolutionKey ?? localName,
|
|
530
554
|
lockEntry,
|
|
531
555
|
versionRange: op.args.versionRange,
|
|
532
556
|
env: persistedEnv,
|
|
533
557
|
enabled,
|
|
534
558
|
...(agents === undefined ? {} : { agents }),
|
|
535
559
|
});
|
|
536
|
-
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
560
|
+
yield* writeEffect;
|
|
561
|
+
const projectionNames = ref.refType === "registry" && lockedVersion !== undefined && lockedVersion !== ref.version
|
|
562
|
+
? [...new Set([...(existingClosure?.localNames ?? []), localName])].sort()
|
|
563
|
+
: [localName];
|
|
564
|
+
const agentSyncResults = yield* Effect.forEach(projectionNames, (projectionName) => Effect.gen(function* () {
|
|
565
|
+
const projectionEntry = projectionName === localName ? settingsEntry : currentMcpServers[projectionName];
|
|
566
|
+
if (projectionEntry === undefined || projectionEntry.kind === "inline") {
|
|
567
|
+
return undefined;
|
|
568
|
+
}
|
|
569
|
+
const projectionSecretIdentity = {
|
|
570
|
+
scopeRoot: path.resolve(ws.baseDir),
|
|
571
|
+
localName: projectionName,
|
|
572
|
+
sourceIdentity,
|
|
573
|
+
};
|
|
574
|
+
const projectionStoredSecrets = yield* loadStoredMcpSecrets(projectionSecretIdentity, secretNames);
|
|
575
|
+
const projectionEnv = projectionName === localName
|
|
576
|
+
? mergedEnv
|
|
577
|
+
: { ...projectionStoredSecrets, ...projectionEntry.env };
|
|
578
|
+
return yield* syncConfiguredAgentsOnInstall({
|
|
579
|
+
wsBaseDir: ws.baseDir,
|
|
580
|
+
scope: ws.scope,
|
|
581
|
+
strict: strictAgentSync,
|
|
582
|
+
serverName: projectionName,
|
|
583
|
+
canonicalPath,
|
|
584
|
+
owner: ref.owner,
|
|
585
|
+
resolvedVersion: ref.version,
|
|
586
|
+
nothingRunnable,
|
|
587
|
+
enabled: projectionEntry.enabled,
|
|
588
|
+
configValues: preserveSecretReferences(projectionEnv, secretNames),
|
|
589
|
+
entry: projectionEntry,
|
|
590
|
+
});
|
|
591
|
+
}), { concurrency: 1 });
|
|
592
|
+
const agentSyncSummaries = agentSyncResults.filter((summary) => summary !== undefined);
|
|
593
|
+
const agentSync = {
|
|
594
|
+
status: agentSyncSummaries.some((summary) => summary.status === "degraded")
|
|
595
|
+
? "degraded"
|
|
596
|
+
: "green",
|
|
597
|
+
details: agentSyncSummaries.flatMap((summary) => summary.details),
|
|
598
|
+
warnings: agentSyncSummaries.flatMap((summary) => summary.warnings),
|
|
599
|
+
outcomes: agentSyncSummaries.flatMap((summary) => summary.outcomes),
|
|
600
|
+
};
|
|
601
|
+
const secretPersistence = yield* persistMcpSecrets(secretIdentity, secretNames, mergedEnv);
|
|
602
|
+
const secretWarnings = secretPersistence.flatMap((outcome) => outcome._tag === "failed"
|
|
603
|
+
? [
|
|
604
|
+
`${outcome.inputName} could not be saved to the system keychain; AXM state was applied and credential action is required`,
|
|
605
|
+
]
|
|
606
|
+
: []);
|
|
607
|
+
const warnings = [...secretWarnings, ...agentSync.warnings];
|
|
541
608
|
const change = currentEntry === undefined ? "created" : "updated";
|
|
542
609
|
const agentOutcomes = agentSync.outcomes.flatMap(({ agentId, outcome }) => outcome._tag === "success" || outcome._tag === "fallback"
|
|
543
610
|
? [
|
|
@@ -549,7 +616,7 @@ export const installMcpServer = (op) => Effect.gen(function* () {
|
|
|
549
616
|
: []);
|
|
550
617
|
return {
|
|
551
618
|
result: "success",
|
|
552
|
-
message: appendWarningsToMessage(`Installed ${ref.server.name} (canonical=success, agent-sync=${agentSync.status})`, warnings),
|
|
619
|
+
message: appendWarningsToMessage(`Installed ${localName} from ${ref.owner}/mcps/${ref.server.name} (canonical=success, agent-sync=${agentSync.status})`, warnings),
|
|
553
620
|
artifact: mcpServerArtifact({
|
|
554
621
|
lockEntry,
|
|
555
622
|
scope: ws.scope,
|
|
@@ -20,6 +20,7 @@ import { acceptedCanonicalObservation, acceptedLockedCanonicalPath, removableAcc
|
|
|
20
20
|
import { agentConfigTarget, mcpServerArtifact, mcpSettingsTarget } from "./artifact.js";
|
|
21
21
|
import { LifecycleFailureAdapter, withAdaptedStepFailures } from "../../failure-adapter.js";
|
|
22
22
|
import { ExtensionLifecycleFailed } from "../../errors.js";
|
|
23
|
+
import { collectSecretInputNames, deleteMcpSecrets, readMcpServerManifest } from "./install.js";
|
|
23
24
|
const REQUIRED_AGENT_IDS = new Set([
|
|
24
25
|
"claude-code",
|
|
25
26
|
"opencode",
|
|
@@ -119,8 +120,8 @@ const syncConfiguredAgentsOnUninstall = (args) => Effect.gen(function* () {
|
|
|
119
120
|
* 3. Remove settings and accepted resolution
|
|
120
121
|
*/
|
|
121
122
|
export const uninstallMcpServer = (op) => Effect.gen(function* () {
|
|
122
|
-
const adapter = yield* LifecycleFailureAdapter;
|
|
123
123
|
const fs = yield* FileSystem.FileSystem;
|
|
124
|
+
const path = yield* Path.Path;
|
|
124
125
|
const ws = yield* WorkspaceMutations;
|
|
125
126
|
const strictAgentSync = Option.getOrElse(op.args.strictAgentSync ?? Option.none(), () => false);
|
|
126
127
|
const desired = yield* ws.getDesiredStateGraph();
|
|
@@ -132,6 +133,12 @@ export const uninstallMcpServer = (op) => Effect.gen(function* () {
|
|
|
132
133
|
});
|
|
133
134
|
}
|
|
134
135
|
const desiredNode = desired.nodes.find((node) => node.type === "mcp-server" && node.name === op.args.serverName);
|
|
136
|
+
const sourceClosure = desiredNode === undefined || desiredNode.authority === "inline"
|
|
137
|
+
? undefined
|
|
138
|
+
: desired.mcpSourceClosures.find((closure) => closure.identity === desiredNode.identity);
|
|
139
|
+
const keepSharedResolution = sourceClosure !== undefined &&
|
|
140
|
+
(sourceClosure.localNames.some((name) => name !== op.args.serverName) ||
|
|
141
|
+
sourceClosure.origins.some((origin) => origin.type === "pack"));
|
|
135
142
|
const acceptedCanonical = yield* acceptedCanonicalObservation({
|
|
136
143
|
workspace: ws,
|
|
137
144
|
type: "mcp-server",
|
|
@@ -150,31 +157,43 @@ export const uninstallMcpServer = (op) => Effect.gen(function* () {
|
|
|
150
157
|
if (desiredNode === undefined && !installedOnDisk) {
|
|
151
158
|
return { result: "success", message: "not installed" };
|
|
152
159
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
if (Option.isSome(removableCanonical))
|
|
160
|
+
const manifest = yield* Option.match(removableCanonical, {
|
|
161
|
+
onNone: () => Effect.succeed(Option.none()),
|
|
162
|
+
onSome: readMcpServerManifest,
|
|
163
|
+
});
|
|
164
|
+
if (!keepSharedResolution && Option.isSome(removableCanonical)) {
|
|
161
165
|
yield* removeIfExists(fs, removableCanonical.value);
|
|
162
|
-
|
|
163
|
-
|
|
166
|
+
}
|
|
167
|
+
// Workspace files and projections are transaction-owned. Keychain cleanup
|
|
168
|
+
// intentionally runs afterward and reports recoverable credential residue.
|
|
169
|
+
yield* ws.removeMcpServer(op.args.serverName);
|
|
164
170
|
const agentSync = yield* syncConfiguredAgentsOnUninstall({
|
|
165
171
|
wsBaseDir: ws.baseDir,
|
|
166
172
|
scope: ws.scope,
|
|
167
173
|
strict: strictAgentSync,
|
|
168
174
|
serverName: op.args.serverName,
|
|
169
175
|
});
|
|
170
|
-
const
|
|
171
|
-
onNone: () =>
|
|
172
|
-
onSome:
|
|
176
|
+
const secretNames = Option.match(manifest, {
|
|
177
|
+
onNone: () => new Set(),
|
|
178
|
+
onSome: collectSecretInputNames,
|
|
173
179
|
});
|
|
180
|
+
const secretDeletion = desiredNode === undefined || desiredNode.authority === "inline"
|
|
181
|
+
? []
|
|
182
|
+
: yield* deleteMcpSecrets({
|
|
183
|
+
scopeRoot: path.resolve(ws.baseDir),
|
|
184
|
+
localName: op.args.serverName,
|
|
185
|
+
sourceIdentity: desiredNode.identity,
|
|
186
|
+
}, secretNames);
|
|
187
|
+
const secretWarnings = secretDeletion.flatMap((outcome) => outcome._tag === "failed"
|
|
188
|
+
? [
|
|
189
|
+
`${outcome.inputName} could not be deleted from the system keychain; AXM state was applied and credential cleanup is required`,
|
|
190
|
+
]
|
|
191
|
+
: []);
|
|
192
|
+
const warnings = [...secretWarnings, ...agentSync.warnings];
|
|
174
193
|
const agentTarget = agentConfigTarget("removed", agentSync.agentIds);
|
|
175
194
|
return {
|
|
176
195
|
result: "success",
|
|
177
|
-
message: appendWarningsToMessage(`Uninstalled ${op.args.serverName} (canonical
|
|
196
|
+
message: appendWarningsToMessage(`Uninstalled ${op.args.serverName} (canonical=${keepSharedResolution ? "retained-shared" : "success"}, agent-sync=${agentSync.status})`, warnings),
|
|
178
197
|
artifact: mcpServerArtifact({
|
|
179
198
|
lockEntry: undefined,
|
|
180
199
|
scope: ws.scope,
|
|
@@ -10,7 +10,7 @@ import * as Layer from "effect/Layer";
|
|
|
10
10
|
import * as Option from "effect/Option";
|
|
11
11
|
import * as Path from "effect/Path";
|
|
12
12
|
import * as Schema from "effect/Schema";
|
|
13
|
-
import { RuleDefinitionInvalid, RuleInstallStateMissing, activeContributors, applyProjectionPlans, planAggregateProjection, reconcileManagedRegionFile, MARKER_KIND_POINT, MARKER_VERSION,
|
|
13
|
+
import { RuleDefinitionInvalid, RuleInstallStateMissing, activeContributors, applyProjectionPlans, planAggregateProjection, reconcileManagedRegionFile, projectionGeneration, MARKER_KIND_POINT, MARKER_VERSION, serializeMarker, assertInstructionTargetsSafe, assertInstructionsGitignoreSafe, observeInstructionProjection, reconcileInstructionTargets, resolveInstructionsConfig, } from "@agentxm/extension-workspace";
|
|
14
14
|
import { decodeExtensionNameSync, formatFqn } from "@agentxm/extension-model/unstable/extensions";
|
|
15
15
|
import { canReuseInstalledPackage, enabledConfiguredEntries, materializeExternalPackageWithTreeIntegrity, } from "@agentxm/extension-workspace";
|
|
16
16
|
import { materializeRegistryPackageWithTreeIntegrity } from "../registry-materialization.js";
|
|
@@ -252,20 +252,21 @@ export const RuleManagerLive = Layer.effect(RuleManager, Effect.gen(function* ()
|
|
|
252
252
|
});
|
|
253
253
|
return sorted;
|
|
254
254
|
}));
|
|
255
|
-
const observedRuleContributors = (content) => content.split(/\r?\n/u).flatMap((line) => {
|
|
256
|
-
const parsed = parseMarker(line, { kind: "block", open: "<!--", close: "-->" });
|
|
257
|
-
if (parsed.state !== "complete" ||
|
|
258
|
-
parsed.marker.kind !== MARKER_KIND_POINT ||
|
|
259
|
-
parsed.marker.pointKind !== "rule") {
|
|
260
|
-
return [];
|
|
261
|
-
}
|
|
262
|
-
const separator = parsed.marker.ext.lastIndexOf("@");
|
|
263
|
-
return separator > 0 ? [parsed.marker.ext.slice(0, separator)] : [];
|
|
264
|
-
});
|
|
265
255
|
const reconcileRulesRegion = (args) => Effect.gen(function* () {
|
|
266
256
|
const { target } = args;
|
|
267
257
|
const contributors = args.input.contributors;
|
|
268
258
|
const rendered = contributors.map(renderRuleBlock).join("\n\n");
|
|
259
|
+
const generation = projectionGeneration([
|
|
260
|
+
"rule-instructions-region-v1",
|
|
261
|
+
target.relative,
|
|
262
|
+
RULES_REGION_OWNER,
|
|
263
|
+
...contributors.flatMap((contributor) => [
|
|
264
|
+
contributor.name,
|
|
265
|
+
contributor.marker,
|
|
266
|
+
contributor.body,
|
|
267
|
+
JSON.stringify(contributor.manifest),
|
|
268
|
+
]),
|
|
269
|
+
]);
|
|
269
270
|
const instructions = args.instructions;
|
|
270
271
|
if (args.dryRun !== true && Option.isSome(instructions)) {
|
|
271
272
|
yield* provide(Effect.gen(function* () {
|
|
@@ -285,6 +286,7 @@ export const RuleManagerLive = Layer.effect(RuleManager, Effect.gen(function* ()
|
|
|
285
286
|
region: RULES_REGION,
|
|
286
287
|
owner: RULES_REGION_OWNER,
|
|
287
288
|
rendered,
|
|
289
|
+
generation,
|
|
288
290
|
...(args.dryRun === undefined ? {} : { dryRun: args.dryRun }),
|
|
289
291
|
writeWhenMissing: true,
|
|
290
292
|
unsupportedTargetDetail: `Instruction source does not support managed regions: ${target.relative}`,
|
|
@@ -302,10 +304,6 @@ export const RuleManagerLive = Layer.effect(RuleManager, Effect.gen(function* ()
|
|
|
302
304
|
present: Option.isSome(observedRegion),
|
|
303
305
|
current: !changed,
|
|
304
306
|
expectedContributors: contributors.map(({ marker }) => marker),
|
|
305
|
-
observedContributors: Option.match(observedRegion, {
|
|
306
|
-
onNone: () => [],
|
|
307
|
-
onSome: observedRuleContributors,
|
|
308
|
-
}),
|
|
309
307
|
};
|
|
310
308
|
if (args.dryRun === true) {
|
|
311
309
|
return {
|
|
@@ -15,7 +15,7 @@ import * as Layer from "effect/Layer";
|
|
|
15
15
|
import * as Option from "effect/Option";
|
|
16
16
|
import * as Schema from "effect/Schema";
|
|
17
17
|
import { WorkspaceMutations } from "@agentxm/workspace-state";
|
|
18
|
-
import { CodingAgentRepository, renderManagedSubagentOutputs, computeSubagentPathsForLayout, subagentContentFilename, subagentContentPath, SubagentContentUnreadable, SubagentDefinitionInvalid, SubagentInstallStateMissing, SubagentIoFailed, warnOnOrphanOverrides, buildRooModeEntry, buildSubagentLockEntry, findManagedSubagentFiles, hasAxmManagedMarker, applyProjectionPlansWithResults, planSingletonProjection, managedSubagentFile, } from "@agentxm/extension-workspace";
|
|
18
|
+
import { CodingAgentRepository, renderManagedSubagentOutputs, managedFileFormatForPath, managedFileMarker, projectionGeneration, computeSubagentPathsForLayout, subagentContentFilename, subagentContentPath, SubagentContentUnreadable, SubagentDefinitionInvalid, SubagentInstallStateMissing, SubagentIoFailed, warnOnOrphanOverrides, buildRooModeEntry, buildSubagentLockEntry, findManagedSubagentFiles, hasAxmManagedMarker, applyProjectionPlansWithResults, planSingletonProjection, managedSubagentFile, } from "@agentxm/extension-workspace";
|
|
19
19
|
import { copyExtensionDirectory } from "@agentxm/extension-workspace";
|
|
20
20
|
import { sanitizeName } from "@agentxm/workspace-state";
|
|
21
21
|
import { stripFileProtocol } from "../internal/fs-helpers.js";
|
|
@@ -80,6 +80,16 @@ export const SubagentManagerLive = Layer.effect(SubagentManager, Effect.gen(func
|
|
|
80
80
|
...args.managedFile,
|
|
81
81
|
helpTopic: "subagents",
|
|
82
82
|
format: "markdown",
|
|
83
|
+
generation: projectionGeneration([
|
|
84
|
+
"subagent-role-skill-v1",
|
|
85
|
+
args.managedFile.ext,
|
|
86
|
+
args.managedFile.source.kind,
|
|
87
|
+
args.managedFile.source.path,
|
|
88
|
+
args.agentId,
|
|
89
|
+
args.name,
|
|
90
|
+
args.description,
|
|
91
|
+
args.body,
|
|
92
|
+
]),
|
|
83
93
|
});
|
|
84
94
|
const jsonValuesEqual = (left, right) => {
|
|
85
95
|
if (left === right)
|
|
@@ -101,6 +111,31 @@ export const SubagentManagerLive = Layer.effect(SubagentManager, Effect.gen(func
|
|
|
101
111
|
}
|
|
102
112
|
return false;
|
|
103
113
|
};
|
|
114
|
+
const serializedJsonValuesEqual = (left, right) => {
|
|
115
|
+
try {
|
|
116
|
+
const leftValue = JSON.parse(left);
|
|
117
|
+
const rightValue = JSON.parse(right);
|
|
118
|
+
return jsonValuesEqual(leftValue, rightValue);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
const generatedFileCurrent = (args) => {
|
|
125
|
+
const format = managedFileFormatForPath(args.outputPath);
|
|
126
|
+
if (format === undefined) {
|
|
127
|
+
return args.outputPath.endsWith(".json")
|
|
128
|
+
? serializedJsonValuesEqual(args.content, args.expected)
|
|
129
|
+
: args.content === args.expected;
|
|
130
|
+
}
|
|
131
|
+
const actualMarker = managedFileMarker(args.content, format);
|
|
132
|
+
const expectedMarker = managedFileMarker(args.expected, format);
|
|
133
|
+
return (Option.isSome(actualMarker) &&
|
|
134
|
+
Option.isSome(expectedMarker) &&
|
|
135
|
+
actualMarker.value.ext === expectedMarker.value.ext &&
|
|
136
|
+
actualMarker.value.src === expectedMarker.value.src &&
|
|
137
|
+
actualMarker.value.generation === expectedMarker.value.generation);
|
|
138
|
+
};
|
|
104
139
|
const materializeRoleSkillFallback = (args) => Effect.gen(function* () {
|
|
105
140
|
const polyfillHash = computeSourceHash(JSON.stringify({ agent: args.agentId, name: args.name, body: args.body }));
|
|
106
141
|
const polyfillDir = path.join(baseDir, ".axm", "build", "polyfills", "subagents", args.sanitized, polyfillHash);
|
|
@@ -530,7 +565,13 @@ export const SubagentManagerLive = Layer.effect(SubagentManager, Effect.gen(func
|
|
|
530
565
|
}
|
|
531
566
|
return Effect.forEach(rendered.outputs, (output) => fs.readFileString(path.resolve(baseDir, output.path)).pipe(Effect.option)).pipe(Effect.map((contents) => ({
|
|
532
567
|
present: contents.every(Option.isSome),
|
|
533
|
-
current: contents.every((content, index) => Option.isSome(content) &&
|
|
568
|
+
current: contents.every((content, index) => Option.isSome(content) &&
|
|
569
|
+
rendered.outputs[index] !== undefined &&
|
|
570
|
+
generatedFileCurrent({
|
|
571
|
+
content: content.value,
|
|
572
|
+
expected: rendered.outputs[index].content,
|
|
573
|
+
outputPath: rendered.outputs[index].path,
|
|
574
|
+
})),
|
|
534
575
|
})));
|
|
535
576
|
}
|
|
536
577
|
if ((ref.fallback ?? manifestFallback) === "none") {
|
|
@@ -555,7 +596,11 @@ export const SubagentManagerLive = Layer.effect(SubagentManager, Effect.gen(func
|
|
|
555
596
|
.readFileString(path.join(path.normalize(skills.dir), sanitized, "SKILL.md"))
|
|
556
597
|
.pipe(Effect.option, Effect.map((content) => ({
|
|
557
598
|
present: Option.isSome(content),
|
|
558
|
-
current: Option.exists(content, (value) =>
|
|
599
|
+
current: Option.exists(content, (value) => generatedFileCurrent({
|
|
600
|
+
content: value,
|
|
601
|
+
expected,
|
|
602
|
+
outputPath: "SKILL.md",
|
|
603
|
+
})),
|
|
559
604
|
})));
|
|
560
605
|
}));
|
|
561
606
|
})));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentxm/extension-lifecycle",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.5",
|
|
4
4
|
"description": "AXM extension-lifecycle feature: install, update, uninstall, enable, and disable policy across root and type-specific command forms for the axm CLI. Unstable and unsupported — use the axm.sh CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "FSL-1.1-MIT",
|
|
@@ -18,10 +18,12 @@
|
|
|
18
18
|
"exports": {
|
|
19
19
|
".": {
|
|
20
20
|
"types": "./dist/src/index.d.ts",
|
|
21
|
+
"axm-source": "./src/index.ts",
|
|
21
22
|
"default": "./dist/src/index.js"
|
|
22
23
|
},
|
|
23
24
|
"./live": {
|
|
24
25
|
"types": "./dist/src/live.d.ts",
|
|
26
|
+
"axm-source": "./src/live.ts",
|
|
25
27
|
"default": "./dist/src/live.js"
|
|
26
28
|
}
|
|
27
29
|
},
|
|
@@ -41,13 +43,13 @@
|
|
|
41
43
|
"dependencies": {
|
|
42
44
|
"effect": "4.0.0-rc.112",
|
|
43
45
|
"semver": "^7.8.5",
|
|
44
|
-
"@agentxm/
|
|
45
|
-
"@agentxm/extension-
|
|
46
|
-
"@agentxm/
|
|
47
|
-
"@agentxm/
|
|
48
|
-
"@agentxm/
|
|
49
|
-
"@agentxm/
|
|
50
|
-
"@agentxm/workspace-
|
|
46
|
+
"@agentxm/extension-model": "^0.28.5",
|
|
47
|
+
"@agentxm/extension-workspace": "^0.28.5",
|
|
48
|
+
"@agentxm/registry-client": "^0.28.5",
|
|
49
|
+
"@agentxm/registry-protocol": "^0.28.5",
|
|
50
|
+
"@agentxm/extension-sources": "^0.28.5",
|
|
51
|
+
"@agentxm/workspace-operations": "^0.28.5",
|
|
52
|
+
"@agentxm/workspace-state": "^0.28.5"
|
|
51
53
|
},
|
|
52
54
|
"devDependencies": {
|
|
53
55
|
"@effect/platform-node": "4.0.0-rc.112",
|