@agentxm/workspace-state 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.
Files changed (30) hide show
  1. package/dist/src/index.d.ts +3 -2
  2. package/dist/src/index.js +3 -2
  3. package/dist/src/lockfile/lockfile.d.ts +3 -3
  4. package/dist/src/lockfile/schema.d.ts +5 -4
  5. package/dist/src/lockfile/schema.js +5 -4
  6. package/dist/src/settings/schema.d.ts +0 -10
  7. package/dist/src/settings/schema.js +1 -22
  8. package/dist/src/workspace/accepted-canonical-ref.d.ts +3 -3
  9. package/dist/src/workspace/accepted-canonical-ref.js +2 -2
  10. package/dist/src/workspace/canonical-observation.js +8 -5
  11. package/dist/src/workspace/configured-agent-outcomes.d.ts +0 -1
  12. package/dist/src/workspace/configured-agent-outcomes.js +0 -11
  13. package/dist/src/workspace/desired-state-graph.d.ts +13 -1
  14. package/dist/src/workspace/desired-state-graph.js +80 -10
  15. package/dist/src/workspace/mcp-entry-semantics.d.ts +2 -5
  16. package/dist/src/workspace/mcp-entry-semantics.js +2 -4
  17. package/dist/src/workspace/mcp-source-identity.d.ts +17 -0
  18. package/dist/src/workspace/mcp-source-identity.js +40 -0
  19. package/dist/src/workspace/read-model/__fixtures__/builder.js +1 -1
  20. package/dist/src/workspace/read-model/errors.d.ts +12 -2
  21. package/dist/src/workspace/read-model/errors.js +3 -0
  22. package/dist/src/workspace/read-model/extensions/mcp-server.js +46 -11
  23. package/dist/src/workspace/read-model/state.js +18 -2
  24. package/dist/src/workspace/read-model/types.d.ts +1 -1
  25. package/dist/src/workspace/read-model-record-readers.js +1 -7
  26. package/dist/src/workspace/service-interface.d.ts +9 -7
  27. package/dist/src/workspace/service.d.ts +241 -10
  28. package/dist/src/workspace/service.js +50 -18
  29. package/dist/src/workspace/test-stubs.js +3 -2
  30. package/package.json +5 -3
@@ -13,6 +13,7 @@ import { configuredAuthoredDirectory } from "./layout.js";
13
13
  import { SETTINGS_FILENAME } from "@agentxm/extension-model/unstable/workspace-files";
14
14
  import { ACQUIRED_EXTENSIONS_DIR } from "./constants.js";
15
15
  import { intersectVersionConstraints } from "@agentxm/extension-model/unstable/version-constraints";
16
+ import { mcpRegistryResolutionKey } from "./mcp-source-identity.js";
16
17
  export const isInlineDesiredExtension = (node) => node.authority === "inline";
17
18
  export const isSourcedDesiredExtension = (node) => node.authority !== "inline";
18
19
  const nodeKey = (type, name) => `${type}:${name}`;
@@ -26,7 +27,17 @@ const registryLocator = (source) => {
26
27
  const ref = source.slice(separator + 1);
27
28
  return ref.startsWith("@") ? { sourceName: source.slice(0, separator), ref } : undefined;
28
29
  };
29
- const sourceIdentity = (type, name, source, settings) => {
30
+ const withVersionConstraint = (source, constraint) => {
31
+ const locator = registryLocator(source);
32
+ if (locator === undefined)
33
+ return source;
34
+ const parsed = parseRegistrySourceRef(locator.ref);
35
+ if (parsed === undefined)
36
+ return source;
37
+ const prefix = source.startsWith("@") ? "" : `${locator.sourceName}:`;
38
+ return `${prefix}${parsed.owner}/${parsed.type}/${parsed.name}@${constraint}`;
39
+ };
40
+ const sourceIdentity = (type, name, source, settings, registryAuthorities) => {
30
41
  if (isWorkspaceSourceLocator(source)) {
31
42
  return settings.owner === undefined
32
43
  ? { identity: source }
@@ -35,8 +46,15 @@ const sourceIdentity = (type, name, source, settings) => {
35
46
  const locator = registryLocator(source);
36
47
  const parsed = locator === undefined ? undefined : parseRegistrySourceRef(locator.ref);
37
48
  if (parsed !== undefined && parsed.type === toExtensionTypePlural(type)) {
49
+ const registryAuthority = locator === undefined ? undefined : registryAuthorities[locator.sourceName];
38
50
  return {
39
- identity: `${parsed.owner}/${parsed.type}/${parsed.name}`,
51
+ identity: type === "mcp-server" && registryAuthority !== undefined
52
+ ? mcpRegistryResolutionKey({
53
+ authority: registryAuthority,
54
+ owner: parsed.owner,
55
+ name: parsed.name,
56
+ })
57
+ : `${parsed.owner}/${parsed.type}/${parsed.name}`,
40
58
  ...(parsed.versionRange === undefined ? {} : { constraint: parsed.versionRange }),
41
59
  };
42
60
  }
@@ -83,6 +101,7 @@ export const collectDesiredConstraintContributors = (_path, origins) => origins
83
101
  source: "settings",
84
102
  range: origin.constraint,
85
103
  location: SETTINGS_FILENAME,
104
+ ...(origin.localName === undefined ? {} : { localName: origin.localName }),
86
105
  },
87
106
  ];
88
107
  }
@@ -113,7 +132,7 @@ const parsePackManifest = (raw) => {
113
132
  const decoded = Schema.decodeUnknownResult(PackManifestSchema)(parsed);
114
133
  return Result.isSuccess(decoded) ? decoded.success : undefined;
115
134
  };
116
- export const buildDesiredStateGraph = ({ baseDir, settings, layout, prospectivePacks = [], }) => Effect.gen(function* () {
135
+ export const buildDesiredStateGraph = ({ baseDir, settings, layout, prospectivePacks = [], registryAuthorities = {}, }) => Effect.gen(function* () {
117
136
  const fs = yield* FileSystem.FileSystem;
118
137
  const path = yield* Path.Path;
119
138
  const candidates = [];
@@ -124,7 +143,7 @@ export const buildDesiredStateGraph = ({ baseDir, settings, layout, prospectiveP
124
143
  const bundled = type === "skill" && entry.origin === "bundled";
125
144
  const identity = bundled
126
145
  ? { identity: `bundled:@agentxm/skills/${name}` }
127
- : sourceIdentity(type, name, entry.source, settings);
146
+ : sourceIdentity(type, name, entry.source, settings, registryAuthorities);
128
147
  if (!bundled && isWorkspaceSourceLocator(entry.source) && settings.owner === undefined) {
129
148
  problems.push({ type: "workspace-owner-missing", extensionType: type, name });
130
149
  }
@@ -138,6 +157,7 @@ export const buildDesiredStateGraph = ({ baseDir, settings, layout, prospectiveP
138
157
  ...(identity.constraint === undefined ? {} : { constraint: identity.constraint }),
139
158
  origin: {
140
159
  type: "settings",
160
+ localName: name,
141
161
  authority: "sourced",
142
162
  source: entry.source,
143
163
  enabled: entry.enabled,
@@ -155,11 +175,16 @@ export const buildDesiredStateGraph = ({ baseDir, settings, layout, prospectiveP
155
175
  identity: `@workspace/mcps/${name}`,
156
176
  authority: "inline",
157
177
  enabled: entry.enabled,
158
- origin: { type: "settings", authority: "inline", enabled: entry.enabled },
178
+ origin: {
179
+ type: "settings",
180
+ localName: name,
181
+ authority: "inline",
182
+ enabled: entry.enabled,
183
+ },
159
184
  });
160
185
  continue;
161
186
  }
162
- const identity = sourceIdentity("mcp-server", name, entry.source, settings);
187
+ const identity = sourceIdentity("mcp-server", name, entry.source, settings, registryAuthorities);
163
188
  if (isWorkspaceSourceLocator(entry.source) && settings.owner === undefined) {
164
189
  problems.push({ type: "workspace-owner-missing", extensionType: "mcp-server", name });
165
190
  }
@@ -173,6 +198,7 @@ export const buildDesiredStateGraph = ({ baseDir, settings, layout, prospectiveP
173
198
  ...(identity.constraint === undefined ? {} : { constraint: identity.constraint }),
174
199
  origin: {
175
200
  type: "settings",
201
+ localName: name,
176
202
  authority: "sourced",
177
203
  source: entry.source,
178
204
  enabled: entry.enabled,
@@ -207,6 +233,7 @@ export const buildDesiredStateGraph = ({ baseDir, settings, layout, prospectiveP
207
233
  ...(identity.constraint === undefined ? {} : { constraint: identity.constraint }),
208
234
  origin: {
209
235
  type: "settings",
236
+ localName: identity.name,
210
237
  authority: "sourced",
211
238
  source: entry.source,
212
239
  enabled: entry.enabled !== false,
@@ -267,10 +294,18 @@ export const buildDesiredStateGraph = ({ baseDir, settings, layout, prospectiveP
267
294
  const parsed = parseExtensionFqnParts(fqn);
268
295
  if (parsed === undefined || parsed.type === "pack")
269
296
  continue;
297
+ const dependencyIdentity = parsed.type === "mcp-server" &&
298
+ registryAuthorities[configuredRegistrySource] !== undefined
299
+ ? mcpRegistryResolutionKey({
300
+ authority: registryAuthorities[configuredRegistrySource],
301
+ owner: parsed.owner,
302
+ name: parsed.name,
303
+ })
304
+ : `${parsed.owner}/${toExtensionTypePlural(parsed.type)}/${parsed.name}`;
270
305
  candidates.push({
271
306
  type: parsed.type,
272
307
  name: parsed.name,
273
- identity: `${parsed.owner}/${toExtensionTypePlural(parsed.type)}/${parsed.name}`,
308
+ identity: dependencyIdentity,
274
309
  authority: "sourced",
275
310
  source: `${fqn}@${constraint}`,
276
311
  enabled: entry.enabled !== false,
@@ -344,7 +379,21 @@ export const buildDesiredStateGraph = ({ baseDir, settings, layout, prospectiveP
344
379
  origins,
345
380
  });
346
381
  }
382
+ const mcpClosuresByIdentity = new Map();
347
383
  for (const node of nodes.values()) {
384
+ if (node.type === "mcp-server" && isSourcedDesiredExtension(node)) {
385
+ const existing = mcpClosuresByIdentity.get(node.identity);
386
+ mcpClosuresByIdentity.set(node.identity, {
387
+ identity: node.identity,
388
+ localNames: [...(existing?.localNames ?? []), node.name].sort(),
389
+ constraints: [
390
+ ...(existing?.constraints ?? []),
391
+ ...node.constraints.filter((constraint) => !(existing?.constraints ?? []).includes(constraint)),
392
+ ],
393
+ origins: [...(existing?.origins ?? []), ...node.origins],
394
+ });
395
+ continue;
396
+ }
348
397
  if (intersectVersionConstraints(node.constraints) === undefined) {
349
398
  problems.push({
350
399
  type: "constraint-conflict",
@@ -355,16 +404,36 @@ export const buildDesiredStateGraph = ({ baseDir, settings, layout, prospectiveP
355
404
  });
356
405
  }
357
406
  }
407
+ const mcpSourceClosures = [...mcpClosuresByIdentity.values()].sort((left, right) => left.identity.localeCompare(right.identity));
408
+ for (const closure of mcpSourceClosures) {
409
+ if (intersectVersionConstraints(closure.constraints) === undefined) {
410
+ problems.push({
411
+ type: "constraint-conflict",
412
+ extensionType: "mcp-server",
413
+ name: closure.localNames.join(", "),
414
+ constraints: closure.constraints,
415
+ contributors: collectDesiredConstraintContributors(path, closure.origins),
416
+ });
417
+ }
418
+ }
358
419
  const typeOrder = new Map(extensionTypes.map((type, index) => [type, index]));
359
420
  const orderedNodes = [...nodes.values()]
360
421
  .map((node) => {
361
422
  if (isInlineDesiredExtension(node))
362
423
  return node;
363
- const constraint = intersectVersionConstraints(node.constraints);
424
+ const constraints = node.type === "mcp-server"
425
+ ? (mcpClosuresByIdentity.get(node.identity)?.constraints ?? node.constraints)
426
+ : node.constraints;
427
+ const constraint = intersectVersionConstraints(constraints);
364
428
  return {
365
429
  ...node,
366
- source: node.identity.startsWith("@") && node.constraints.length > 0 && constraint !== undefined
367
- ? `${node.identity}@${constraint}`
430
+ constraints,
431
+ source: constraints.length > 0 && constraint !== undefined
432
+ ? node.type === "mcp-server"
433
+ ? withVersionConstraint(node.source, constraint)
434
+ : node.identity.startsWith("@")
435
+ ? `${node.identity}@${constraint}`
436
+ : node.source
368
437
  : node.source,
369
438
  };
370
439
  })
@@ -378,6 +447,7 @@ export const buildDesiredStateGraph = ({ baseDir, settings, layout, prospectiveP
378
447
  return {
379
448
  complete: problems.length === 0,
380
449
  nodes: orderedNodes,
450
+ mcpSourceClosures,
381
451
  problems,
382
452
  };
383
453
  });
@@ -1,17 +1,14 @@
1
1
  /**
2
2
  * Settings-semantics predicates for MCP server entries.
3
3
  *
4
- * Pure derivations over workspace-owned MCP entry shapes: per-agent targeting
5
- * of a configured settings entry, and AXM ownership/provenance metadata
6
- * embedded in agent-native MCP config entries.
4
+ * Pure derivations over workspace-owned MCP entry shapes: AXM
5
+ * ownership/provenance metadata embedded in agent-native MCP config entries.
7
6
  *
8
7
  * @experimental This API is unstable and may change without notice.
9
8
  * @packageDocumentation
10
9
  */
11
10
  import * as Option from "effect/Option";
12
11
  import * as Schema from "effect/Schema";
13
- import type { McpServerEntry } from "../settings/schema.js";
14
- export declare const isMcpServerApplicableToAgent: (entry: McpServerEntry, agentId: string) => boolean;
15
12
  export declare const AXM_MCP_METADATA_KEY = "x-axm";
16
13
  export declare const AxmMcpMetadataSchema: Schema.Union<readonly [Schema.Struct<{
17
14
  readonly v: Schema.Literal<1>;
@@ -1,16 +1,14 @@
1
1
  /**
2
2
  * Settings-semantics predicates for MCP server entries.
3
3
  *
4
- * Pure derivations over workspace-owned MCP entry shapes: per-agent targeting
5
- * of a configured settings entry, and AXM ownership/provenance metadata
6
- * embedded in agent-native MCP config entries.
4
+ * Pure derivations over workspace-owned MCP entry shapes: AXM
5
+ * ownership/provenance metadata embedded in agent-native MCP config entries.
7
6
  *
8
7
  * @experimental This API is unstable and may change without notice.
9
8
  * @packageDocumentation
10
9
  */
11
10
  import * as Option from "effect/Option";
12
11
  import * as Schema from "effect/Schema";
13
- export const isMcpServerApplicableToAgent = (entry, agentId) => entry.agents === undefined || entry.agents.some((candidate) => candidate === agentId);
14
12
  export const AXM_MCP_METADATA_KEY = "x-axm";
15
13
  const ResolvableSourceTypeSchema = Schema.Literals([
16
14
  "github",
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Canonical identity for one accepted MCP source-resolution closure.
3
+ *
4
+ * Connection names deliberately do not participate: several local MCP
5
+ * connections may share this identity and therefore one accepted resolution.
6
+ */
7
+ import type { Handle } from "@agentxm/extension-model/unstable/extensions/handle";
8
+ import type { ExtensionName } from "@agentxm/extension-model/unstable/extensions/common";
9
+ import type { McpServerLockEntry } from "../lockfile/schema.js";
10
+ export declare const mcpRegistryResolutionKey: (args: {
11
+ readonly authority: URL | string;
12
+ readonly owner: Handle | string;
13
+ readonly name: ExtensionName | string;
14
+ }) => string;
15
+ /** Deterministic lock-map key for every currently accepted MCP source class. */
16
+ export declare const mcpResolutionKey: (entry: McpServerLockEntry) => string;
17
+ //# sourceMappingURL=mcp-source-identity.d.ts.map
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Canonical identity for one accepted MCP source-resolution closure.
3
+ *
4
+ * Connection names deliberately do not participate: several local MCP
5
+ * connections may share this identity and therefore one accepted resolution.
6
+ */
7
+ const normalizeAuthority = (authority) => {
8
+ const raw = authority instanceof URL ? authority.href : authority;
9
+ try {
10
+ const normalized = new URL(raw).href;
11
+ return normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
12
+ }
13
+ catch {
14
+ return raw.endsWith("/") ? raw.slice(0, -1) : raw;
15
+ }
16
+ };
17
+ const encodeIdentityPart = (value) => encodeURIComponent(value);
18
+ export const mcpRegistryResolutionKey = (args) => `registry:${encodeIdentityPart(normalizeAuthority(args.authority))}:${args.owner}/mcps/${args.name}`;
19
+ /** Deterministic lock-map key for every currently accepted MCP source class. */
20
+ export const mcpResolutionKey = (entry) => {
21
+ switch (entry.type) {
22
+ case "registry":
23
+ return mcpRegistryResolutionKey({
24
+ authority: entry.endpoint,
25
+ owner: entry.owner,
26
+ name: entry.name,
27
+ });
28
+ case "github":
29
+ case "gitlab":
30
+ case "bitbucket":
31
+ return `${entry.type}:${encodeIdentityPart(normalizeAuthority(entry.endpoint))}:${entry.owner}/${entry.repo}:${entry.packageOwner ?? ""}/mcps/${entry.packageName}`;
32
+ case "azurerepos":
33
+ return `azurerepos:${encodeIdentityPart(normalizeAuthority(entry.endpoint))}:${entry.organization}/${entry.project}/${entry.repo}:${entry.packageOwner ?? ""}/mcps/${entry.packageName}`;
34
+ case "git":
35
+ return `git:${encodeIdentityPart(normalizeAuthority(entry.url))}:${entry.packageOwner ?? ""}/mcps/${entry.packageName}`;
36
+ case "local":
37
+ return `local:${encodeIdentityPart(entry.path)}:${entry.packageOwner ?? ""}/mcps/${entry.packageName}`;
38
+ }
39
+ };
40
+ //# sourceMappingURL=mcp-source-identity.js.map
@@ -362,7 +362,7 @@ const validSettingsContents = {
362
362
  skills: { "managed-tool": { source: "github:owner/repo", enabled: true } },
363
363
  };
364
364
  const validLockfileContents = {
365
- lockfileVersion: 6,
365
+ lockfileVersion: 7,
366
366
  skills: {
367
367
  "managed-tool": {
368
368
  type: "github",
@@ -59,8 +59,18 @@ export declare class LockfileDecodeError extends LockfileDecodeError_base<{
59
59
  readonly raw: unknown;
60
60
  }> {
61
61
  }
62
- /** Lockfile-read failure union (IO, parse, decode). */
63
- export type LockfileReadError = LockfileIoError | LockfileParseError | LockfileDecodeError;
62
+ declare const LockfileVersionUnsupported_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
63
+ readonly _tag: "LockfileVersionUnsupported";
64
+ } & Readonly<A>;
65
+ /** A syntactically valid lockfile declares a version this CLI cannot read. */
66
+ export declare class LockfileVersionUnsupported extends LockfileVersionUnsupported_base<{
67
+ readonly path: string;
68
+ readonly observedVersion: number;
69
+ readonly supportedVersion: number;
70
+ }> {
71
+ }
72
+ /** Lockfile-read failure union (IO, parse, decode, unsupported version). */
73
+ export type LockfileReadError = LockfileIoError | LockfileParseError | LockfileDecodeError | LockfileVersionUnsupported;
64
74
  declare const WorkspaceRootEscape_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
65
75
  readonly _tag: "WorkspaceRootEscape";
66
76
  } & Readonly<A>;
@@ -18,6 +18,9 @@ export class LockfileParseError extends Data.TaggedError("LockfileParseError") {
18
18
  /** Lockfile schema decode failure; `raw` carries the parsed value. */
19
19
  export class LockfileDecodeError extends Data.TaggedError("LockfileDecodeError") {
20
20
  }
21
+ /** A syntactically valid lockfile declares a version this CLI cannot read. */
22
+ export class LockfileVersionUnsupported extends Data.TaggedError("LockfileVersionUnsupported") {
23
+ }
21
24
  /** Provider-construction error: workspace root escapes the configured allowed root. */
22
25
  export class WorkspaceRootEscape extends Data.TaggedError("WorkspaceRootEscape") {
23
26
  }
@@ -10,6 +10,7 @@
10
10
  import * as Effect from "effect/Effect";
11
11
  import * as Option from "effect/Option";
12
12
  import { decodeExtensionNameSync, } from "@agentxm/extension-model/unstable/extensions/common";
13
+ import { parseSourceQualifiedRegistrySourcePatternParts } from "@agentxm/extension-model/unstable/extensions";
13
14
  import { filterMapOccurrences } from "./actual-helpers.js";
14
15
  import { canonicalAxmPackageRoot } from "./package-root.js";
15
16
  import { makeProjectedSubjectCells, projectInstalledExtensions, } from "./projection.js";
@@ -24,18 +25,39 @@ const declaredFromSettings = (settings) => {
24
25
  entry,
25
26
  }));
26
27
  };
27
- const resolvedFromLockfile = (lockfile) => {
28
- if (lockfile.mcpServers === undefined)
29
- return [];
30
- return Object.entries(lockfile.mcpServers).map(([name, lockEntry]) => ({
31
- name: decodeExtensionNameSync(name),
32
- lockEntry,
33
- }));
28
+ const resolvedFromState = (settings, lockfile, packs) => {
29
+ const locked = Object.values(lockfile.mcpServers ?? {});
30
+ const resolved = [];
31
+ const names = new Set();
32
+ for (const [localName, entry] of Object.entries(settings.mcpServers ?? {})) {
33
+ if (entry.kind === "inline")
34
+ continue;
35
+ const parsed = parseSourceQualifiedRegistrySourcePatternParts(entry.source);
36
+ const lockEntry = locked.find((candidate) => parsed !== undefined && candidate.type === "registry"
37
+ ? candidate.sourceName === parsed.sourceName &&
38
+ candidate.owner === parsed.owner &&
39
+ candidate.name === parsed.name
40
+ : candidate.workspaceName === localName);
41
+ if (lockEntry === undefined)
42
+ continue;
43
+ names.add(localName);
44
+ resolved.push({ name: decodeExtensionNameSync(localName), lockEntry });
45
+ }
46
+ for (const member of packs.flatMap((pack) => pack.mcpServers)) {
47
+ if (names.has(member.name))
48
+ continue;
49
+ const lockEntry = locked.find((candidate) => candidate.workspaceName === member.name);
50
+ if (lockEntry === undefined)
51
+ continue;
52
+ names.add(member.name);
53
+ resolved.push({ name: member.name, lockEntry });
54
+ }
55
+ return resolved;
34
56
  };
35
- const canonicalToActual = (occ, scope) => {
57
+ const canonicalToActual = (occ, scope, localName = occ.name) => {
36
58
  const packageRoot = canonicalAxmPackageRoot(occ);
37
59
  return {
38
- key: { scope, type: "mcp-server", name: occ.name },
60
+ key: { scope, type: "mcp-server", name: localName },
39
61
  origin: occ.origin === "canonical-axm"
40
62
  ? { _tag: "canonical-axm-mcp-server" }
41
63
  : { _tag: "external-axm-mcp-server" },
@@ -94,11 +116,24 @@ const mcpServerPolicy = (scope) => ({
94
116
  export const makeMcpServerExtensionsApi = (deps) => Effect.gen(function* () {
95
117
  const { scope, loaders, scanners, installedPacks, diagnostics } = deps;
96
118
  const declared = loaders.settings.pipe(Effect.map((opt) => Option.map(opt, declaredFromSettings)));
97
- const resolved = loaders.lockfile.pipe(Effect.map((opt) => Option.map(opt, resolvedFromLockfile)));
119
+ const resolved = Effect.all({
120
+ settings: loaders.settings.pipe(Effect.catch(() => Effect.succeed(Option.none()))),
121
+ lockfile: loaders.lockfile,
122
+ packs: installedPacks.pipe(Effect.catch(() => Effect.succeed([]))),
123
+ }).pipe(Effect.map(({ settings, lockfile, packs }) => Option.all({ settings, lockfile }).pipe(Option.map(({ settings: decodedSettings, lockfile: decodedLockfile }) => resolvedFromState(decodedSettings, decodedLockfile, packs)))));
98
124
  const actual = Effect.gen(function* () {
99
125
  const canonical = yield* scanners.canonical;
100
126
  const mcpConfig = yield* scanners.mcpConfig;
101
- const fromCanonical = filterMapOccurrences(canonical, "mcp-server", (occ) => canonicalToActual(occ, scope));
127
+ const accepted = yield* resolved.pipe(Effect.catch(() => Effect.succeed(Option.none())));
128
+ const resolvedEntries = Option.getOrElse(accepted, () => []);
129
+ const fromCanonical = filterMapOccurrences(canonical, "mcp-server", (occ) => occ).flatMap((occ) => {
130
+ const matchingNames = resolvedEntries
131
+ .filter((entry) => entry.lockEntry.type === "registry" && entry.lockEntry.name === occ.name)
132
+ .map((entry) => entry.name);
133
+ return matchingNames.length === 0
134
+ ? [canonicalToActual(occ, scope)]
135
+ : matchingNames.map((name) => canonicalToActual(occ, scope, name));
136
+ });
102
137
  const fromMcpConfig = mcpConfig.map((occ) => mcpConfigToActual(occ, scope));
103
138
  return [...fromCanonical, ...fromMcpConfig];
104
139
  });
@@ -3,10 +3,10 @@ import * as Effect from "effect/Effect";
3
3
  import * as Option from "effect/Option";
4
4
  import * as Schema from "effect/Schema";
5
5
  import YAML from "yaml";
6
- import { LockfileSchema } from "../../lockfile/schema.js";
6
+ import { LOCKFILE_VERSION, LockfileSchema } from "../../lockfile/schema.js";
7
7
  import { formatSchemaIssuesToLines } from "@agentxm/extension-model/unstable/schema-issues";
8
8
  import { SettingsSchema } from "../../settings/schema.js";
9
- import { LockfileDecodeError, LockfileIoError, LockfileParseError, SettingsDecodeError, SettingsIoError, SettingsParseError, } from "./errors.js";
9
+ import { LockfileDecodeError, LockfileIoError, LockfileParseError, LockfileVersionUnsupported, SettingsDecodeError, SettingsIoError, SettingsParseError, } from "./errors.js";
10
10
  // ---------------------------------------------------------------------------
11
11
  // Raw bytes loaders (shared by the decoded loaders and the public raw cells)
12
12
  // ---------------------------------------------------------------------------
@@ -71,6 +71,22 @@ const loadLockfile = (rawCell) => Effect.gen(function* () {
71
71
  try: () => YAML.parse(bytes),
72
72
  catch: (cause) => new LockfileParseError({ path, raw: bytes, cause }),
73
73
  });
74
+ if (typeof parsed === "object" &&
75
+ parsed !== null &&
76
+ !Array.isArray(parsed) &&
77
+ Object.hasOwn(parsed, "lockfileVersion")) {
78
+ const observedVersion = Reflect.get(parsed, "lockfileVersion");
79
+ if (typeof observedVersion === "number" &&
80
+ Number.isSafeInteger(observedVersion) &&
81
+ observedVersion > 0 &&
82
+ observedVersion !== LOCKFILE_VERSION) {
83
+ return yield* Effect.fail(new LockfileVersionUnsupported({
84
+ path,
85
+ observedVersion,
86
+ supportedVersion: LOCKFILE_VERSION,
87
+ }));
88
+ }
89
+ }
74
90
  const decoded = yield* Schema.decodeUnknownEffect(LockfileSchema)(parsed).pipe(Effect.mapError((error) => new LockfileDecodeError({
75
91
  path,
76
92
  issues: formatSchemaIssuesToLines(error.issue),
@@ -63,7 +63,7 @@ export interface InstalledPackRef {
63
63
  *
64
64
  * The failure channels are intentionally narrow: `declared` only fails with
65
65
  * `SettingsReadError` (3 tags), `resolved` only fails with
66
- * `LockfileReadError` (3 tags), and `actual` never fails — workspace-root
66
+ * `LockfileReadError` (4 tags), and `actual` never fails — workspace-root
67
67
  * path-escape is validated once at provider construction (Layer-level), not
68
68
  * per cell.
69
69
  */
@@ -7,7 +7,7 @@
7
7
  import * as Effect from "effect/Effect";
8
8
  import * as Option from "effect/Option";
9
9
  import { installableExtensionTypes, } from "@agentxm/extension-model/unstable/extensions/installable-types";
10
- import { isAxmManagedMcpEntry, isMcpServerApplicableToAgent } from "./mcp-entry-semantics.js";
10
+ import { isAxmManagedMcpEntry } from "./mcp-entry-semantics.js";
11
11
  import { createDefaultSettings } from "../settings/index.js";
12
12
  import { configuredAgentLifecycleOutcomes } from "./configured-agent-outcomes.js";
13
13
  import { projectExtensionInventory, } from "./read-model/extensions/inventory.js";
@@ -298,7 +298,6 @@ export const makeReadModelRecordReaders = (args) => {
298
298
  const configuredAgents = settings.agents ?? [];
299
299
  const finalizeInventory = (inventory) => {
300
300
  const withOutcomes = inventory.items.map((row) => {
301
- const mcpEntry = row.type === "mcp-server" ? settings.mcpServers?.[row.name] : undefined;
302
301
  return {
303
302
  ...row,
304
303
  agentOutcomes: row.classification.lifecycle === "unmanaged"
@@ -312,11 +311,6 @@ export const makeReadModelRecordReaders = (args) => {
312
311
  targetState: row.enabled === false ? "disabled" : "enabled",
313
312
  installed: row.installed,
314
313
  observedAgentIds: row.agents,
315
- ...(mcpEntry === undefined
316
- ? {}
317
- : {
318
- applicableAgentIds: configuredAgents.filter((agentId) => isMcpServerApplicableToAgent(mcpEntry, agentId)),
319
- }),
320
314
  }),
321
315
  };
322
316
  });
@@ -33,7 +33,6 @@ import type { ExtensionInventory } from "./read-model/extensions/inventory.js";
33
33
  import type { ResolvedKnowledgeDiscoveryConfig } from "../knowledge/discovery-config.js";
34
34
  import type { DesiredStateGraph, ProspectivePackRef } from "./desired-state-graph.js";
35
35
  import type { AbsolutePath } from "@agentxm/extension-model/unstable/path-types";
36
- import type { ConfigurableAgentId } from "@agentxm/extension-model/unstable/agent-capabilities";
37
36
  import type { WorkspaceLayout } from "./layout.js";
38
37
  import type { ExtensionPathSource } from "./extension-paths.js";
39
38
  /**
@@ -212,11 +211,12 @@ export interface SetSubagentArgs {
212
211
  */
213
212
  export interface SetMcpServerArgs {
214
213
  readonly name: string;
214
+ /** Canonical source-resolution key. Unlike name, this is not connection-scoped. */
215
+ readonly resolutionKey: string;
215
216
  readonly lockEntry: McpServerLockEntry;
216
217
  readonly versionRange: Option.Option<string>;
217
218
  readonly env?: Readonly<Record<string, string>>;
218
219
  readonly enabled?: boolean;
219
- readonly agents?: ReadonlyArray<ConfigurableAgentId>;
220
220
  }
221
221
  /**
222
222
  * Arguments for `setRule` -- bundles the rule name with the lock entry.
@@ -408,8 +408,10 @@ export interface WorkspaceMutationsService {
408
408
  readonly removeSubagentLock: (name: string) => Effect.Effect<void, WorkspaceLockfileMutationFailure>;
409
409
  /** Read lockfile and return the MCP servers lock map. */
410
410
  readonly getLockedMcpServers: () => Effect.Effect<McpServersLockMap, WorkspaceLockfileReadFailure>;
411
- /** Read lockfile and return the entry for a specific MCP server, or Option.none(). */
412
- readonly getLockedMcpServer: (name: string) => Effect.Effect<Option.Option<McpServerLockEntry>, WorkspaceLockfileReadFailure>;
411
+ /** Read lockfile and return an MCP accepted resolution by source-resolution identity. */
412
+ readonly getLockedMcpServer: (resolutionKey: string) => Effect.Effect<Option.Option<McpServerLockEntry>, WorkspaceLockfileReadFailure>;
413
+ /** Resolve a local MCP connection name to its shared accepted resolution. */
414
+ readonly getLockedMcpServerForConnection: (localName: string) => Effect.Effect<Option.Option<McpServerLockEntry>, WorkspaceStateReadFailure>;
413
415
  /** Update desired MCP settings and any external accepted resolution atomically. */
414
416
  readonly setMcpServer: (args: SetMcpServerArgs) => Effect.Effect<void, WorkspaceStateMutationFailure>;
415
417
  /** Add or update an MCP server in lockfile only (skip settings). Used for pack dependencies. Serialized by semaphore. */
@@ -469,11 +471,11 @@ export interface WorkspaceMutationsOptions {
469
471
  readonly allowUninitialized?: boolean;
470
472
  }
471
473
  /**
472
- * Error loading workspace mutations: scoped settings reads, layout
473
- * resolution, and the initialization gate.
474
+ * Error loading workspace mutations: scoped settings and lockfile reads,
475
+ * layout resolution, and the initialization gate.
474
476
  *
475
477
  * @experimental This API is unstable and may change without notice.
476
478
  */
477
- export type WorkspaceMutationsError = WorkspaceSettingsReadFailure | WorkspaceLayoutError | WorkspaceNotInitialized;
479
+ export type WorkspaceMutationsError = WorkspaceSettingsReadFailure | WorkspaceLockfileReadFailure | WorkspaceLayoutError | WorkspaceNotInitialized;
478
480
  export {};
479
481
  //# sourceMappingURL=service-interface.d.ts.map