@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.
- package/dist/src/index.d.ts +3 -2
- package/dist/src/index.js +3 -2
- package/dist/src/lockfile/lockfile.d.ts +3 -3
- package/dist/src/lockfile/schema.d.ts +5 -4
- package/dist/src/lockfile/schema.js +5 -4
- package/dist/src/settings/schema.d.ts +0 -10
- package/dist/src/settings/schema.js +1 -22
- package/dist/src/workspace/accepted-canonical-ref.d.ts +3 -3
- package/dist/src/workspace/accepted-canonical-ref.js +2 -2
- package/dist/src/workspace/canonical-observation.js +8 -5
- package/dist/src/workspace/configured-agent-outcomes.d.ts +0 -1
- package/dist/src/workspace/configured-agent-outcomes.js +0 -11
- package/dist/src/workspace/desired-state-graph.d.ts +13 -1
- package/dist/src/workspace/desired-state-graph.js +80 -10
- package/dist/src/workspace/mcp-entry-semantics.d.ts +2 -5
- package/dist/src/workspace/mcp-entry-semantics.js +2 -4
- package/dist/src/workspace/mcp-source-identity.d.ts +17 -0
- package/dist/src/workspace/mcp-source-identity.js +40 -0
- package/dist/src/workspace/read-model/__fixtures__/builder.js +1 -1
- package/dist/src/workspace/read-model/errors.d.ts +12 -2
- package/dist/src/workspace/read-model/errors.js +3 -0
- package/dist/src/workspace/read-model/extensions/mcp-server.js +46 -11
- package/dist/src/workspace/read-model/state.js +18 -2
- package/dist/src/workspace/read-model/types.d.ts +1 -1
- package/dist/src/workspace/read-model-record-readers.js +1 -7
- package/dist/src/workspace/service-interface.d.ts +9 -7
- package/dist/src/workspace/service.d.ts +241 -10
- package/dist/src/workspace/service.js +50 -18
- package/dist/src/workspace/test-stubs.js +3 -2
- 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
|
|
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:
|
|
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: {
|
|
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:
|
|
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
|
|
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
|
-
|
|
367
|
-
|
|
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:
|
|
5
|
-
*
|
|
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:
|
|
5
|
-
*
|
|
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:
|
|
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
|
-
|
|
63
|
-
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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:
|
|
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 =
|
|
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
|
|
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` (
|
|
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
|
|
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
|
|
412
|
-
readonly getLockedMcpServer: (
|
|
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,
|
|
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
|