@agentxm/extension-lifecycle 0.28.4 → 0.28.6
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 +24 -12
- package/dist/src/index.d.ts +1 -3
- package/dist/src/index.js +1 -3
- package/dist/src/knowledge/manager.js +61 -16
- package/dist/src/mcps/manager.js +40 -6
- package/dist/src/mcps/operations/disable.js +6 -42
- package/dist/src/mcps/operations/enable.js +1 -34
- package/dist/src/mcps/operations/install.d.ts +22 -3
- package/dist/src/mcps/operations/install.js +112 -82
- package/dist/src/mcps/operations/uninstall.js +34 -15
- package/dist/src/registry-materialization.js +5 -0
- package/dist/src/rules/manager.js +16 -16
- package/dist/src/subagents/manager.js +48 -3
- package/dist/src/workflows/install-command/workflow.d.ts +1 -2
- package/dist/src/workflows/install-command/workflow.js +6 -6
- package/package.json +10 -8
- package/dist/src/resolution-progress.d.ts +0 -18
- package/dist/src/resolution-progress.js +0 -11
- package/dist/src/skills/operations/uninstall.d.ts +0 -40
- package/dist/src/skills/operations/uninstall.js +0 -157
|
@@ -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 {
|
|
24
|
-
import {
|
|
23
|
+
import { createHash } from "node:crypto";
|
|
24
|
+
import { CodingAgentRepository, applyProjectionPlansWithResults, planSingletonProjection, } from "@agentxm/extension-workspace";
|
|
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";
|
|
@@ -32,7 +33,6 @@ import { printSourceParams } from "@agentxm/extension-model/unstable/sources/pri
|
|
|
32
33
|
import { computeMaterializedTreeIntegrity } from "@agentxm/workspace-state";
|
|
33
34
|
import { decodeVersionSync } from "@agentxm/extension-model/unstable/version-constraints";
|
|
34
35
|
import { MCP_SERVER_MANIFEST_FILENAME, McpServerManifestSchema, } from "@agentxm/extension-model/unstable/mcps/manifest-schema";
|
|
35
|
-
import { isMcpServerApplicableToAgent } from "@agentxm/workspace-state";
|
|
36
36
|
import { agentConfigTargets, mcpServerArtifact, mcpSettingsTarget, mcpSourceTarget, } from "./artifact.js";
|
|
37
37
|
import { LifecycleFailureAdapter, withAdaptedStepFailures } from "../../failure-adapter.js";
|
|
38
38
|
import { ExtensionLifecycleFailed } from "../../errors.js";
|
|
@@ -55,7 +55,9 @@ const buildLockEntry = (ref, treeIntegrity) => ({
|
|
|
55
55
|
treeIntegrity,
|
|
56
56
|
});
|
|
57
57
|
const MCP_SECRET_SERVICE = "axm-mcp";
|
|
58
|
-
const mcpSecretAccount = (
|
|
58
|
+
export const mcpSecretAccount = (args) => createHash("sha256")
|
|
59
|
+
.update([args.scopeRoot, args.localName, args.sourceIdentity, args.inputName].join("\0"))
|
|
60
|
+
.digest("hex");
|
|
59
61
|
const keyringModuleSpecifier = ["@napi-rs", "keyring"].join("/");
|
|
60
62
|
const loadKeyringEntry = Effect.tryPromise({
|
|
61
63
|
try: async () => {
|
|
@@ -64,22 +66,22 @@ const loadKeyringEntry = Effect.tryPromise({
|
|
|
64
66
|
},
|
|
65
67
|
catch: () => undefined,
|
|
66
68
|
});
|
|
67
|
-
const saveMcpSecret = (
|
|
69
|
+
const saveMcpSecret = (account, inputName, value) => Effect.gen(function* () {
|
|
68
70
|
const Entry = yield* loadKeyringEntry;
|
|
69
71
|
return yield* Effect.try({
|
|
70
72
|
try: () => {
|
|
71
|
-
const entry = new Entry(MCP_SECRET_SERVICE,
|
|
73
|
+
const entry = new Entry(MCP_SECRET_SERVICE, account);
|
|
72
74
|
entry.setPassword(value);
|
|
73
75
|
return { _tag: "saved", inputName };
|
|
74
76
|
},
|
|
75
77
|
catch: () => undefined,
|
|
76
78
|
});
|
|
77
79
|
}).pipe(Effect.catch(() => Effect.succeed({ _tag: "failed", inputName })));
|
|
78
|
-
const loadMcpSecret = (
|
|
80
|
+
const loadMcpSecret = (account) => Effect.gen(function* () {
|
|
79
81
|
const Entry = yield* loadKeyringEntry;
|
|
80
82
|
return yield* Effect.try({
|
|
81
83
|
try: () => {
|
|
82
|
-
const entry = new Entry(MCP_SECRET_SERVICE,
|
|
84
|
+
const entry = new Entry(MCP_SECRET_SERVICE, account);
|
|
83
85
|
return Option.fromNullOr(entry.getPassword());
|
|
84
86
|
},
|
|
85
87
|
catch: () => undefined,
|
|
@@ -116,7 +118,7 @@ const collectRequiredInputNames = (manifest) => {
|
|
|
116
118
|
}
|
|
117
119
|
return names;
|
|
118
120
|
};
|
|
119
|
-
const collectSecretInputNames = (manifest) => {
|
|
121
|
+
export const collectSecretInputNames = (manifest) => {
|
|
120
122
|
const names = new Set();
|
|
121
123
|
const add = (input) => {
|
|
122
124
|
const name = maybeSecretInputName(input);
|
|
@@ -179,7 +181,7 @@ const installFromRegistry = (ref, reuse) => Effect.gen(function* () {
|
|
|
179
181
|
}
|
|
180
182
|
return canonicalPath;
|
|
181
183
|
});
|
|
182
|
-
const
|
|
184
|
+
export const readMcpServerManifest = (canonicalPath) => Effect.gen(function* () {
|
|
183
185
|
const fs = yield* FileSystem.FileSystem;
|
|
184
186
|
const path = yield* Path.Path;
|
|
185
187
|
const manifestPath = path.join(canonicalPath, MCP_SERVER_MANIFEST_FILENAME);
|
|
@@ -204,8 +206,8 @@ const isNothingRunnableManifest = (manifest) => Option.match(manifest, {
|
|
|
204
206
|
onSome: (value) => (value.server.packages === undefined || value.server.packages.length === 0) &&
|
|
205
207
|
(value.server.remotes === undefined || value.server.remotes.length === 0),
|
|
206
208
|
});
|
|
207
|
-
const loadStoredMcpSecrets = (
|
|
208
|
-
const entries = yield* Effect.forEach(secretNames, (name) => loadMcpSecret(
|
|
209
|
+
const loadStoredMcpSecrets = (identity, secretNames) => Effect.gen(function* () {
|
|
210
|
+
const entries = yield* Effect.forEach(secretNames, (name) => loadMcpSecret(mcpSecretAccount({ ...identity, inputName: name })).pipe(Effect.map((value) => ({ name, value }))), { concurrency: "unbounded" });
|
|
209
211
|
const loaded = {};
|
|
210
212
|
for (const { name, value } of entries) {
|
|
211
213
|
if (Option.isSome(value))
|
|
@@ -213,12 +215,24 @@ const loadStoredMcpSecrets = (serverName, secretNames) => Effect.gen(function* (
|
|
|
213
215
|
}
|
|
214
216
|
return loaded;
|
|
215
217
|
});
|
|
216
|
-
const persistMcpSecrets = (
|
|
218
|
+
const persistMcpSecrets = (identity, secretNames, values) => Effect.forEach(secretNames, (name) => {
|
|
217
219
|
const value = values[name];
|
|
218
220
|
return value === undefined
|
|
219
221
|
? Effect.succeed({ _tag: "skipped", inputName: name })
|
|
220
|
-
: saveMcpSecret(
|
|
222
|
+
: saveMcpSecret(mcpSecretAccount({ ...identity, inputName: name }), name, value);
|
|
221
223
|
}, { concurrency: "unbounded" });
|
|
224
|
+
export const deleteMcpSecrets = (identity, secretNames) => Effect.forEach(secretNames, (inputName) => Effect.gen(function* () {
|
|
225
|
+
const Entry = yield* loadKeyringEntry;
|
|
226
|
+
return yield* Effect.try({
|
|
227
|
+
try: () => {
|
|
228
|
+
const entry = new Entry(MCP_SECRET_SERVICE, mcpSecretAccount({ ...identity, inputName }));
|
|
229
|
+
return entry.deletePassword()
|
|
230
|
+
? { _tag: "deleted", inputName }
|
|
231
|
+
: { _tag: "absent", inputName };
|
|
232
|
+
},
|
|
233
|
+
catch: () => undefined,
|
|
234
|
+
});
|
|
235
|
+
}).pipe(Effect.catch(() => Effect.succeed({ _tag: "failed", inputName }))), { concurrency: "unbounded" });
|
|
222
236
|
const redactSettingsEnv = (values, secretNames) => {
|
|
223
237
|
const redacted = {};
|
|
224
238
|
for (const [name, value] of Object.entries(values)) {
|
|
@@ -280,17 +294,6 @@ const syncConfiguredAgentsOnInstall = (args) => Effect.gen(function* () {
|
|
|
280
294
|
warnings.push(`Skipping unknown configured agents: ${unknownConfiguredAgentIds.join(", ")}`);
|
|
281
295
|
}
|
|
282
296
|
const configuredAgents = yield* agentRepo.getConfiguredAgents();
|
|
283
|
-
const sharedTargetConflict = sharedMcpTargetPolicyConflict({
|
|
284
|
-
entry: args.entry,
|
|
285
|
-
agentIds: configuredAgents.map((agent) => agent.id),
|
|
286
|
-
scope: args.scope,
|
|
287
|
-
});
|
|
288
|
-
if (sharedTargetConflict !== undefined) {
|
|
289
|
-
return yield* new ExtensionLifecycleFailed({
|
|
290
|
-
category: "conflict",
|
|
291
|
-
detail: sharedTargetConflict,
|
|
292
|
-
});
|
|
293
|
-
}
|
|
294
297
|
let outcomes;
|
|
295
298
|
if (args.nothingRunnable) {
|
|
296
299
|
outcomes = configuredAgents.map((agent) => ({
|
|
@@ -316,28 +319,6 @@ const syncConfiguredAgentsOnInstall = (args) => Effect.gen(function* () {
|
|
|
316
319
|
observedContributors: [],
|
|
317
320
|
}),
|
|
318
321
|
apply: () => Effect.gen(function* () {
|
|
319
|
-
if (!isMcpServerApplicableToAgent(args.entry, agent.id)) {
|
|
320
|
-
const inspection = yield* inspectAgentMcpServer({
|
|
321
|
-
workspaceRoot: args.wsBaseDir,
|
|
322
|
-
scope: args.scope,
|
|
323
|
-
agentId: agent.id,
|
|
324
|
-
serverName: args.serverName,
|
|
325
|
-
entry: args.entry,
|
|
326
|
-
});
|
|
327
|
-
if (inspection.status === "unmanaged") {
|
|
328
|
-
return yield* new McpSharedTargetConflict({
|
|
329
|
-
reason: `${agent.id} has an unmanaged MCP server named ${args.serverName}; AXM will not remove it while applying the target policy`,
|
|
330
|
-
});
|
|
331
|
-
}
|
|
332
|
-
const outcome = inspection.status === "drift"
|
|
333
|
-
? yield* agent.removeMcpServer({
|
|
334
|
-
workspaceRoot: args.wsBaseDir,
|
|
335
|
-
scope: args.scope,
|
|
336
|
-
serverName: args.serverName,
|
|
337
|
-
})
|
|
338
|
-
: { _tag: "success", targets: [] };
|
|
339
|
-
return { agentId: agent.id, outcome };
|
|
340
|
-
}
|
|
341
322
|
const outcome = yield* agent.addMcpServer({
|
|
342
323
|
workspaceRoot: args.wsBaseDir,
|
|
343
324
|
scope: args.scope,
|
|
@@ -398,9 +379,10 @@ const syncConfiguredAgentsOnInstall = (args) => Effect.gen(function* () {
|
|
|
398
379
|
* then update lockfile/settings.
|
|
399
380
|
*/
|
|
400
381
|
export const installMcpServer = (op) => Effect.gen(function* () {
|
|
401
|
-
const adapter = yield* LifecycleFailureAdapter;
|
|
402
382
|
const ws = yield* WorkspaceMutations;
|
|
383
|
+
const path = yield* Path.Path;
|
|
403
384
|
const { ref } = op.args;
|
|
385
|
+
const localName = op.args.localName ?? ref.server.name;
|
|
404
386
|
if (ref.refType !== "registry" && ref.refType !== "workspace") {
|
|
405
387
|
return yield* new ExtensionLifecycleFailed({
|
|
406
388
|
category: "usage",
|
|
@@ -415,8 +397,26 @@ export const installMcpServer = (op) => Effect.gen(function* () {
|
|
|
415
397
|
}
|
|
416
398
|
const strictAgentSync = Option.getOrElse(op.args.strictAgentSync ?? Option.none(), () => false);
|
|
417
399
|
const env = Option.getOrElse(op.args.env ?? Option.none(), () => ({}));
|
|
400
|
+
const resolutionKey = ref.refType === "registry"
|
|
401
|
+
? mcpRegistryResolutionKey({
|
|
402
|
+
authority: ref.source.location,
|
|
403
|
+
owner: ref.owner,
|
|
404
|
+
name: ref.server.name,
|
|
405
|
+
})
|
|
406
|
+
: undefined;
|
|
407
|
+
const sourceIdentity = resolutionKey ?? `workspace:${ref.owner}/mcps/${ref.server.name}`;
|
|
408
|
+
const desiredGraph = yield* ws.getDesiredStateGraph();
|
|
409
|
+
const existingLocalNode = desiredGraph.nodes.find((node) => node.type === "mcp-server" && node.name === localName);
|
|
410
|
+
if (existingLocalNode !== undefined &&
|
|
411
|
+
(existingLocalNode.authority === "inline" || existingLocalNode.identity !== sourceIdentity)) {
|
|
412
|
+
return yield* new ExtensionLifecycleFailed({
|
|
413
|
+
category: "conflict",
|
|
414
|
+
detail: `Local MCP name "${localName}" is already owned by a different source`,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
const existingClosure = desiredGraph.mcpSourceClosures.find((closure) => closure.identity === sourceIdentity);
|
|
418
418
|
const lockedVersion = ref.refType === "registry"
|
|
419
|
-
? acceptedRegistryVersionForRef(yield* ws.getLockedMcpServer(
|
|
419
|
+
? acceptedRegistryVersionForRef(yield* ws.getLockedMcpServer(resolutionKey ?? ""), ref)
|
|
420
420
|
: undefined;
|
|
421
421
|
const canonicalPath = ref.refType === "registry"
|
|
422
422
|
? yield* installFromRegistry(ref, { force: op.args.force, lockedVersion })
|
|
@@ -446,7 +446,7 @@ export const installMcpServer = (op) => Effect.gen(function* () {
|
|
|
446
446
|
}
|
|
447
447
|
return ref.location;
|
|
448
448
|
});
|
|
449
|
-
const manifest = yield*
|
|
449
|
+
const manifest = yield* readMcpServerManifest(canonicalPath);
|
|
450
450
|
const nothingRunnable = isNothingRunnableManifest(manifest);
|
|
451
451
|
const secretNames = Option.match(manifest, {
|
|
452
452
|
onNone: () => new Set(),
|
|
@@ -459,8 +459,13 @@ export const installMcpServer = (op) => Effect.gen(function* () {
|
|
|
459
459
|
? buildLockEntry(ref, yield* computeMaterializedTreeIntegrity(canonicalPath))
|
|
460
460
|
: undefined;
|
|
461
461
|
const currentMcpServers = yield* ws.getConfiguredMcpServerEntries();
|
|
462
|
-
const currentEntry = currentMcpServers[
|
|
463
|
-
const
|
|
462
|
+
const currentEntry = currentMcpServers[localName];
|
|
463
|
+
const secretIdentity = {
|
|
464
|
+
scopeRoot: path.resolve(ws.baseDir),
|
|
465
|
+
localName,
|
|
466
|
+
sourceIdentity,
|
|
467
|
+
};
|
|
468
|
+
const storedSecrets = yield* loadStoredMcpSecrets(secretIdentity, secretNames);
|
|
464
469
|
const mergedEnv = { ...storedSecrets, ...(currentEntry?.env ?? {}), ...env };
|
|
465
470
|
// Under --non-interactive there is nobody to prompt, so a required input
|
|
466
471
|
// that nothing supplied would otherwise install a server that cannot start.
|
|
@@ -475,7 +480,7 @@ export const installMcpServer = (op) => Effect.gen(function* () {
|
|
|
475
480
|
if (missingInputs.length > 0 && op.args.nonInteractive) {
|
|
476
481
|
return yield* new ExtensionLifecycleFailed({
|
|
477
482
|
category: "usage",
|
|
478
|
-
detail: `${
|
|
483
|
+
detail: `${localName} needs ${missingInputs.join(", ")}, and --non-interactive cannot prompt for them`,
|
|
479
484
|
suggestions: [
|
|
480
485
|
{
|
|
481
486
|
description: "Supply each required input on the command line",
|
|
@@ -486,58 +491,83 @@ export const installMcpServer = (op) => Effect.gen(function* () {
|
|
|
486
491
|
}
|
|
487
492
|
const persistedEnv = redactSettingsEnv(mergedEnv, secretNames);
|
|
488
493
|
const enabled = currentEntry?.enabled ?? true;
|
|
489
|
-
const agents = op.args.agents ?? currentEntry?.agents;
|
|
490
494
|
const settingsEntry = {
|
|
491
495
|
kind: "sourced",
|
|
492
496
|
source: ref.refType === "workspace" ? "workspace" : printSourceParams(ref.source),
|
|
493
497
|
env: persistedEnv,
|
|
494
498
|
enabled,
|
|
495
|
-
...(agents === undefined ? {} : { agents }),
|
|
496
499
|
};
|
|
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
500
|
const writeEffect = op.args.skipStateWrites === true
|
|
515
501
|
? Effect.void
|
|
516
502
|
: Option.getOrElse(op.args.skipSettings, () => false)
|
|
517
503
|
? lockEntry === undefined
|
|
518
504
|
? Effect.void
|
|
519
505
|
: ws.setMcpServerLock({
|
|
520
|
-
name: ref.server.name,
|
|
506
|
+
name: resolutionKey ?? ref.server.name,
|
|
507
|
+
resolutionKey: resolutionKey ?? ref.server.name,
|
|
521
508
|
lockEntry,
|
|
522
509
|
versionRange: Option.none(),
|
|
523
510
|
})
|
|
524
511
|
: lockEntry === undefined
|
|
525
|
-
? ws.setMcpServerEntry(
|
|
512
|
+
? ws.setMcpServerEntry(localName, {
|
|
526
513
|
...settingsEntry,
|
|
527
514
|
})
|
|
528
515
|
: ws.setMcpServer({
|
|
529
|
-
name:
|
|
516
|
+
name: localName,
|
|
517
|
+
resolutionKey: resolutionKey ?? localName,
|
|
530
518
|
lockEntry,
|
|
531
519
|
versionRange: op.args.versionRange,
|
|
532
520
|
env: persistedEnv,
|
|
533
521
|
enabled,
|
|
534
|
-
...(agents === undefined ? {} : { agents }),
|
|
535
522
|
});
|
|
536
|
-
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
523
|
+
yield* writeEffect;
|
|
524
|
+
const projectionNames = ref.refType === "registry" && lockedVersion !== undefined && lockedVersion !== ref.version
|
|
525
|
+
? [...new Set([...(existingClosure?.localNames ?? []), localName])].sort()
|
|
526
|
+
: [localName];
|
|
527
|
+
const agentSyncResults = yield* Effect.forEach(projectionNames, (projectionName) => Effect.gen(function* () {
|
|
528
|
+
const projectionEntry = projectionName === localName ? settingsEntry : currentMcpServers[projectionName];
|
|
529
|
+
if (projectionEntry === undefined || projectionEntry.kind === "inline") {
|
|
530
|
+
return undefined;
|
|
531
|
+
}
|
|
532
|
+
const projectionSecretIdentity = {
|
|
533
|
+
scopeRoot: path.resolve(ws.baseDir),
|
|
534
|
+
localName: projectionName,
|
|
535
|
+
sourceIdentity,
|
|
536
|
+
};
|
|
537
|
+
const projectionStoredSecrets = yield* loadStoredMcpSecrets(projectionSecretIdentity, secretNames);
|
|
538
|
+
const projectionEnv = projectionName === localName
|
|
539
|
+
? mergedEnv
|
|
540
|
+
: { ...projectionStoredSecrets, ...projectionEntry.env };
|
|
541
|
+
return yield* syncConfiguredAgentsOnInstall({
|
|
542
|
+
wsBaseDir: ws.baseDir,
|
|
543
|
+
scope: ws.scope,
|
|
544
|
+
strict: strictAgentSync,
|
|
545
|
+
serverName: projectionName,
|
|
546
|
+
canonicalPath,
|
|
547
|
+
owner: ref.owner,
|
|
548
|
+
resolvedVersion: ref.version,
|
|
549
|
+
nothingRunnable,
|
|
550
|
+
enabled: projectionEntry.enabled,
|
|
551
|
+
configValues: preserveSecretReferences(projectionEnv, secretNames),
|
|
552
|
+
entry: projectionEntry,
|
|
553
|
+
});
|
|
554
|
+
}), { concurrency: 1 });
|
|
555
|
+
const agentSyncSummaries = agentSyncResults.filter((summary) => summary !== undefined);
|
|
556
|
+
const agentSync = {
|
|
557
|
+
status: agentSyncSummaries.some((summary) => summary.status === "degraded")
|
|
558
|
+
? "degraded"
|
|
559
|
+
: "green",
|
|
560
|
+
details: agentSyncSummaries.flatMap((summary) => summary.details),
|
|
561
|
+
warnings: agentSyncSummaries.flatMap((summary) => summary.warnings),
|
|
562
|
+
outcomes: agentSyncSummaries.flatMap((summary) => summary.outcomes),
|
|
563
|
+
};
|
|
564
|
+
const secretPersistence = yield* persistMcpSecrets(secretIdentity, secretNames, mergedEnv);
|
|
565
|
+
const secretWarnings = secretPersistence.flatMap((outcome) => outcome._tag === "failed"
|
|
566
|
+
? [
|
|
567
|
+
`${outcome.inputName} could not be saved to the system keychain; AXM state was applied and credential action is required`,
|
|
568
|
+
]
|
|
569
|
+
: []);
|
|
570
|
+
const warnings = [...secretWarnings, ...agentSync.warnings];
|
|
541
571
|
const change = currentEntry === undefined ? "created" : "updated";
|
|
542
572
|
const agentOutcomes = agentSync.outcomes.flatMap(({ agentId, outcome }) => outcome._tag === "success" || outcome._tag === "fallback"
|
|
543
573
|
? [
|
|
@@ -549,7 +579,7 @@ export const installMcpServer = (op) => Effect.gen(function* () {
|
|
|
549
579
|
: []);
|
|
550
580
|
return {
|
|
551
581
|
result: "success",
|
|
552
|
-
message: appendWarningsToMessage(`Installed ${ref.server.name} (canonical=success, agent-sync=${agentSync.status})`, warnings),
|
|
582
|
+
message: appendWarningsToMessage(`Installed ${localName} from ${ref.owner}/mcps/${ref.server.name} (canonical=success, agent-sync=${agentSync.status})`, warnings),
|
|
553
583
|
artifact: mcpServerArtifact({
|
|
554
584
|
lockEntry,
|
|
555
585
|
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,
|
|
@@ -17,6 +17,7 @@ import { computeIntegrity } from "./internal/integrity.js";
|
|
|
17
17
|
import { ArchiveIntegrityMismatch, recoverCanonicalDirectory, replaceCanonicalDirectoryWithInspection, } from "@agentxm/extension-workspace";
|
|
18
18
|
import { CoupledDependencyFailure } from "@agentxm/extension-workspace";
|
|
19
19
|
import { coupleLifecycleDependencyFailure } from "./errors.js";
|
|
20
|
+
import { makeThrottledUnitProgress } from "@agentxm/workspace-operations";
|
|
20
21
|
import { computeMaterializedTreeIntegrity, } from "@agentxm/workspace-state";
|
|
21
22
|
const registryLocationForClient = (location) => location.protocol === "file:" ? location.pathname : location.href;
|
|
22
23
|
/**
|
|
@@ -35,12 +36,16 @@ export const materializeRegistryPackageWithTreeIntegrity = (args) => Effect.gen(
|
|
|
35
36
|
canonicalPath: args.destinationPath,
|
|
36
37
|
});
|
|
37
38
|
const client = yield* createRegistryClient(registryLocationForClient(args.sourceLocation));
|
|
39
|
+
// Continuous download progress reaches the lifecycle broadcast throttled:
|
|
40
|
+
// tens of events per archive, attributed to the unit that is running.
|
|
41
|
+
const reportProgress = yield* makeThrottledUnitProgress({ unit: "bytes" });
|
|
38
42
|
const { archive } = yield* client
|
|
39
43
|
.getExtensionPackage({
|
|
40
44
|
owner: args.owner,
|
|
41
45
|
type: args.type,
|
|
42
46
|
name: args.name,
|
|
43
47
|
version: Option.some(args.version),
|
|
48
|
+
onProgress: (progress) => reportProgress(progress.done, progress.total),
|
|
44
49
|
})
|
|
45
50
|
.pipe(Effect.mapError(coupleLifecycleDependencyFailure));
|
|
46
51
|
if (Option.isSome(args.integrity)) {
|
|
@@ -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 {
|
|
@@ -336,7 +334,9 @@ export const RuleManagerLive = Layer.effect(RuleManager, Effect.gen(function* ()
|
|
|
336
334
|
unitId: "rule:instructions-region",
|
|
337
335
|
targetFile: target.absolute,
|
|
338
336
|
graph,
|
|
339
|
-
|
|
337
|
+
// Rule contributors are decided from desired state alone, so the
|
|
338
|
+
// instructions region never excludes one.
|
|
339
|
+
select: (completeGraph) => selectRuleContributors({ graph: completeGraph, locked }).pipe(Effect.map((contributors) => ({ contributors, exclusions: [] }))),
|
|
340
340
|
adapter: {
|
|
341
341
|
observe: (input) => reconcileRulesRegion({ input, target, instructions, dryRun: true }).pipe(Effect.map(({ projectionUnitObservation }) => projectionUnitObservation)),
|
|
342
342
|
apply: (input) => reconcileRulesRegion({ input, target, instructions }).pipe(Effect.asVoid),
|
|
@@ -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
|
})));
|
|
@@ -11,7 +11,6 @@ import type * as Scope from "effect/Scope";
|
|
|
11
11
|
import type { Plan } from "@agentxm/workspace-operations";
|
|
12
12
|
import type { OperationResolution } from "@agentxm/workspace-operations";
|
|
13
13
|
import type { PlanExecution } from "@agentxm/workspace-operations";
|
|
14
|
-
import { LifecycleResolutionProgress } from "../../resolution-progress.js";
|
|
15
14
|
/**
|
|
16
15
|
* Type-specific actions for install command workflows.
|
|
17
16
|
*
|
|
@@ -53,5 +52,5 @@ export declare const runInstallCommandWorkflow: <Args, Parsed, Req, Ref, Intent,
|
|
|
53
52
|
readonly execution: PlanExecution;
|
|
54
53
|
readonly transformIntent?: (intent: Intent) => Intent;
|
|
55
54
|
readonly transformPlan?: (plan: Plan) => Effect.Effect<Plan, TransformError, TransformRequirements>;
|
|
56
|
-
}) => Effect.Effect<OperationResolution<never>, import("@agentxm/workspace-state").WorkspaceSettingsReadFailure | ParseError | ResolveError | TransformError | import("@agentxm/workspace-operations").CandidateFingerprintFailed | import("@agentxm/workspace-operations").ApprovalRecoveryMissing | import("@agentxm/workspace-operations").PlanInteractionFailed | import("@agentxm/workspace-state").WorkspaceTransitionAcquireFailure | import("@agentxm/workspace-state").LockfileValidationError, import("@agentxm/workspace-state").WorkspaceMutations | import("effect/FileSystem").FileSystem | import("effect/Path").Path |
|
|
55
|
+
}) => Effect.Effect<OperationResolution<never>, import("@agentxm/workspace-state").WorkspaceSettingsReadFailure | ParseError | ResolveError | TransformError | import("@agentxm/workspace-operations").CandidateFingerprintFailed | import("@agentxm/workspace-operations").ApprovalRecoveryMissing | import("@agentxm/workspace-operations").PlanInteractionFailed | import("@agentxm/workspace-state").WorkspaceTransitionAcquireFailure | import("@agentxm/workspace-state").LockfileValidationError, import("@agentxm/workspace-state").WorkspaceMutations | import("effect/FileSystem").FileSystem | import("effect/Path").Path | import("@agentxm/workspace-operations").ResolvePlanInteraction | Exclude<TransformRequirements, Scope.Scope>>;
|
|
57
56
|
//# sourceMappingURL=workflow.d.ts.map
|