@dotdrelle/wiki-manager 0.15.73 → 0.15.74
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/package.json +1 -1
- package/src/commands/slash.js +8 -0
- package/src/core/buildInfo.json +2 -2
- package/src/core/env.js +17 -2
- package/src/core/env.test.js +31 -0
- package/src/core/mcp.js +1 -1
- package/src/core/startupCheck.js +12 -3
- package/src/core/startupCheck.test.js +28 -0
- package/src/orchestrator/providers/deepAgentsProvider.js +30 -3
- package/src/orchestrator/providers/deepAgentsProvider.test.js +32 -4
- package/src/orchestrator/providers/runtimeProviders.js +28 -2
- package/src/orchestrator/providers/runtimeProviders.test.js +48 -0
package/package.json
CHANGED
package/src/commands/slash.js
CHANGED
|
@@ -682,6 +682,14 @@ function runtimeProvidersSection(session) {
|
|
|
682
682
|
// never truncates and stays scannable.
|
|
683
683
|
lines.push('capabilities:');
|
|
684
684
|
for (const capability of capabilities) lines.push(` ${capability}`);
|
|
685
|
+
// Configured in agent-runtimes.json but not served by the gateway (the
|
|
686
|
+
// list above is the intersection): say so, one per line, or the operator
|
|
687
|
+
// reads a shorter list without knowing the gateway lost its /config.
|
|
688
|
+
const drift = (session.runtimeProviderDrift ?? []).find((item) => item.runtimeId === runtimeId);
|
|
689
|
+
if (drift && drift.missing.length > 0) {
|
|
690
|
+
lines.push('not served by the gateway:');
|
|
691
|
+
for (const capability of drift.missing) lines.push(` ${capability}`);
|
|
692
|
+
}
|
|
685
693
|
}
|
|
686
694
|
return sectionBlock('Agentic runtime', lines);
|
|
687
695
|
}
|
package/src/core/buildInfo.json
CHANGED
package/src/core/env.js
CHANGED
|
@@ -11,6 +11,13 @@ const TEMPLATE_AUTHORING_CHAT_TOOLS = [
|
|
|
11
11
|
'wiki_outline', 'template_read', 'template_write', 'build_context_write',
|
|
12
12
|
'wiki_read_deliverable',
|
|
13
13
|
];
|
|
14
|
+
// Same additive rule for the packaged cme allow-list: an install scaffolded
|
|
15
|
+
// before the live search tools existed keeps the three legacy reads forever,
|
|
16
|
+
// and /chat can never call cme_confluence_search — the LLM is offered a
|
|
17
|
+
// server with no search and loops. A hand-edited list (not a superset of the
|
|
18
|
+
// legacy default) is the operator's and stays untouched.
|
|
19
|
+
const LEGACY_DEFAULT_CME_CHAT_TOOLS = ['cme_status', 'cme_sources_list', 'cme_export_status'];
|
|
20
|
+
const CME_SEARCH_CHAT_TOOLS = ['cme_confluence_search', 'cme_wiki_search'];
|
|
14
21
|
|
|
15
22
|
export function userManagerDir() {
|
|
16
23
|
return process.cwd();
|
|
@@ -148,17 +155,25 @@ export function ensureManagerScaffold({ log = () => {} } = {}) {
|
|
|
148
155
|
const missingWikiChatTools = migrateWikiChatTools
|
|
149
156
|
? TEMPLATE_AUTHORING_CHAT_TOOLS.filter((tool) => !wikiAllow.includes(tool))
|
|
150
157
|
: [];
|
|
158
|
+
const cmeAllow = current.chatAccess?.servers?.cme?.allow;
|
|
159
|
+
const migrateCmeChatTools = Array.isArray(cmeAllow)
|
|
160
|
+
&& LEGACY_DEFAULT_CME_CHAT_TOOLS.every((tool) => cmeAllow.includes(tool));
|
|
161
|
+
const missingCmeChatTools = migrateCmeChatTools
|
|
162
|
+
? CME_SEARCH_CHAT_TOOLS.filter((tool) => !cmeAllow.includes(tool))
|
|
163
|
+
: [];
|
|
151
164
|
if (missing.length > 0) {
|
|
152
165
|
for (const key of missing) current[key] = example[key];
|
|
153
166
|
}
|
|
154
167
|
for (const key of missingServers) currentServers[key] = exampleServers[key];
|
|
155
168
|
wikiAllow?.push(...missingWikiChatTools);
|
|
156
|
-
|
|
169
|
+
cmeAllow?.push(...missingCmeChatTools);
|
|
170
|
+
const missingChatTools = [...missingWikiChatTools, ...missingCmeChatTools];
|
|
171
|
+
if (missing.length > 0 || missingServers.length > 0 || missingChatTools.length > 0) {
|
|
157
172
|
writeFileSync(endpointsFile, `${JSON.stringify(current, null, 2)}\n`);
|
|
158
173
|
const changes = [
|
|
159
174
|
missing.length > 0 ? `keys: ${missing.join(', ')}` : '',
|
|
160
175
|
missingServers.length > 0 ? `servers: ${missingServers.join(', ')}` : '',
|
|
161
|
-
|
|
176
|
+
missingChatTools.length > 0 ? `chat tools: ${missingChatTools.join(', ')}` : '',
|
|
162
177
|
].filter(Boolean).join('; ');
|
|
163
178
|
created.push(`mcp.endpoints.json ${changes}`);
|
|
164
179
|
}
|
package/src/core/env.test.js
CHANGED
|
@@ -141,6 +141,37 @@ test('scaffold upgrades the packaged wiki chat allow-list with template authorin
|
|
|
141
141
|
});
|
|
142
142
|
});
|
|
143
143
|
|
|
144
|
+
test('scaffold upgrades the packaged cme chat allow-list with the live search tools', () => {
|
|
145
|
+
withTempManagerDir((dir) => {
|
|
146
|
+
const endpointsFile = join(dir, 'mcp.endpoints.json');
|
|
147
|
+
const example = JSON.parse(readFileSync('mcp.endpoints.example.json', 'utf8'));
|
|
148
|
+
example.chatAccess.servers.cme.allow = ['cme_status', 'cme_sources_list', 'cme_export_status'];
|
|
149
|
+
writeFileSync(endpointsFile, JSON.stringify(example, null, 2));
|
|
150
|
+
|
|
151
|
+
const changes = ensureManagerScaffold();
|
|
152
|
+
const after = JSON.parse(readFileSync(endpointsFile, 'utf8'));
|
|
153
|
+
|
|
154
|
+
assert.ok(changes.some((item) => item.includes('cme_confluence_search')));
|
|
155
|
+
assert.deepEqual(after.chatAccess.servers.cme.allow, [
|
|
156
|
+
'cme_status', 'cme_sources_list', 'cme_export_status', 'cme_confluence_search', 'cme_wiki_search',
|
|
157
|
+
]);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('scaffold leaves a custom cme chat allow-list untouched', () => {
|
|
162
|
+
withTempManagerDir((dir) => {
|
|
163
|
+
const endpointsFile = join(dir, 'mcp.endpoints.json');
|
|
164
|
+
writeFileSync(endpointsFile, JSON.stringify({
|
|
165
|
+
mcpServers: {},
|
|
166
|
+
chatAccess: { servers: { cme: { allow: ['cme_status'] } } },
|
|
167
|
+
}, null, 2));
|
|
168
|
+
|
|
169
|
+
ensureManagerScaffold();
|
|
170
|
+
const after = JSON.parse(readFileSync(endpointsFile, 'utf8'));
|
|
171
|
+
assert.deepEqual(after.chatAccess.servers.cme.allow, ['cme_status']);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
144
175
|
test('scaffold leaves a custom wiki chat allow-list untouched', () => {
|
|
145
176
|
withTempManagerDir((dir) => {
|
|
146
177
|
const endpointsFile = join(dir, 'mcp.endpoints.json');
|
package/src/core/mcp.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { managerEnvFile, managerMcpEndpointsFile, readEnvFile } from './env.js';
|
|
3
3
|
|
|
4
|
-
const WIKI_MANAGER_VERSION = '0.15.
|
|
4
|
+
const WIKI_MANAGER_VERSION = '0.15.74';
|
|
5
5
|
|
|
6
6
|
function envValue(key) {
|
|
7
7
|
const filePath = managerEnvFile();
|
package/src/core/startupCheck.js
CHANGED
|
@@ -357,13 +357,22 @@ export async function checkAgenticRuntimes({
|
|
|
357
357
|
for (const { id, provider } of providers) {
|
|
358
358
|
try {
|
|
359
359
|
const description = await provider.describe();
|
|
360
|
+
const down = description.health === 'unavailable';
|
|
361
|
+
// Ask the runtime what it serves — describe() alone reflects /health,
|
|
362
|
+
// not the capability set, and a gateway that lost its config answers
|
|
363
|
+
// healthy with one default capability.
|
|
364
|
+
const capabilities = down ? [] : await provider.discoverCapabilities();
|
|
365
|
+
const missing = provider?.lastDiscovery?.missing ?? [];
|
|
360
366
|
runtimes.push({
|
|
361
367
|
name: id,
|
|
362
|
-
status:
|
|
363
|
-
capabilities: (
|
|
368
|
+
status: down ? 'failed' : 'connected',
|
|
369
|
+
capabilities: (capabilities ?? [])
|
|
364
370
|
.map((capability) => capability?.name ?? capability?.id ?? '')
|
|
365
371
|
.filter(Boolean),
|
|
366
|
-
|
|
372
|
+
...(missing.length > 0 ? { missingCapabilities: [...missing] } : {}),
|
|
373
|
+
reason: down
|
|
374
|
+
? (description.error ?? 'unavailable')
|
|
375
|
+
: (missing.length > 0 ? `does not serve configured: ${missing.join(', ')}` : null),
|
|
367
376
|
});
|
|
368
377
|
} catch (err) {
|
|
369
378
|
runtimes.push({ name: id, status: 'failed', reason: commandError(err) });
|
|
@@ -327,3 +327,31 @@ test('checkAgenticRuntimes reports a down runtime as failed', async () => {
|
|
|
327
327
|
assert.equal(result.context.runtimes[0].status, 'failed');
|
|
328
328
|
assert.match(result.detail, /pending/);
|
|
329
329
|
});
|
|
330
|
+
|
|
331
|
+
test('checkAgenticRuntimes asks the runtime what it serves and reports configured capabilities it does not', async () => {
|
|
332
|
+
const provider = {
|
|
333
|
+
runtime: 'deepagents',
|
|
334
|
+
lastDiscovery: null,
|
|
335
|
+
async describe() { return { runtime: 'deepagents', health: 'available', version: 't', protocolVersion: '1' }; },
|
|
336
|
+
async discoverCapabilities() {
|
|
337
|
+
this.lastDiscovery = { served: ['agent.review'], configured: ['agent.review', 'agent.notify'], missing: ['agent.notify'], extra: [] };
|
|
338
|
+
return [{ name: 'agent.review', operations: ['run'] }];
|
|
339
|
+
},
|
|
340
|
+
async execute() { throw new Error('unused'); },
|
|
341
|
+
async status() { throw new Error('unused'); },
|
|
342
|
+
async cancel() {},
|
|
343
|
+
async approve() {},
|
|
344
|
+
subscribe() { return () => {}; },
|
|
345
|
+
};
|
|
346
|
+
const result = await checkAgenticRuntimes({
|
|
347
|
+
loadConfig: () => [{ id: 'deepagents', type: 'deepagents', enabled: true }],
|
|
348
|
+
resolve: () => ({ providers: [{ id: 'deepagents', type: 'deepagents', provider }], skipped: [] }),
|
|
349
|
+
env: {},
|
|
350
|
+
});
|
|
351
|
+
assert.equal(result.ok, true);
|
|
352
|
+
const runtime = result.context.runtimes[0];
|
|
353
|
+
assert.equal(runtime.status, 'connected');
|
|
354
|
+
assert.deepEqual(runtime.capabilities, ['agent.review'], 'the served set, not the configured one');
|
|
355
|
+
assert.deepEqual(runtime.missingCapabilities, ['agent.notify']);
|
|
356
|
+
assert.match(runtime.reason, /does not serve configured: agent\.notify/);
|
|
357
|
+
});
|
|
@@ -56,7 +56,10 @@ export function createDeepAgentsProvider({
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
const provider = {
|
|
60
|
+
// Filled by discoverCapabilities: what the gateway served vs what the
|
|
61
|
+
// config declares. `null` until the first discovery.
|
|
62
|
+
lastDiscovery: null,
|
|
60
63
|
async describe() {
|
|
61
64
|
let health = 'available';
|
|
62
65
|
let runtimeVersion = version ?? null;
|
|
@@ -77,10 +80,33 @@ export function createDeepAgentsProvider({
|
|
|
77
80
|
...(lastError ? { error: lastError } : {}),
|
|
78
81
|
};
|
|
79
82
|
},
|
|
83
|
+
// The gateway is ALWAYS asked what it serves. The static config (the
|
|
84
|
+
// manager's agent-runtimes.json entry) carries the richer metadata the
|
|
85
|
+
// resolver routes on — mutationClass, aliases, aliasOperations — but it is
|
|
86
|
+
// a declaration, not an observation: a gateway that lost its /config
|
|
87
|
+
// mount degrades to its built-in default and the manager, trusting the
|
|
88
|
+
// config alone, kept listing eight capabilities it could not govern.
|
|
89
|
+
// Only the capabilities BOTH declare are offered; the difference is
|
|
90
|
+
// exposed as `lastDiscovery` so /status and the runtime log can say why a
|
|
91
|
+
// configured capability is missing.
|
|
80
92
|
async discoverCapabilities() {
|
|
81
|
-
if (Array.isArray(capabilities)) return capabilities;
|
|
82
93
|
const list = await httpJson('/capabilities');
|
|
83
|
-
|
|
94
|
+
const served = (Array.isArray(list) ? list : [])
|
|
95
|
+
.filter((item) => item && typeof item === 'object' && typeof item.name === 'string' && item.name.trim());
|
|
96
|
+
const servedNames = served.map((item) => item.name);
|
|
97
|
+
if (!Array.isArray(capabilities)) {
|
|
98
|
+
provider.lastDiscovery = { served: servedNames, configured: null, missing: [], extra: [] };
|
|
99
|
+
return served;
|
|
100
|
+
}
|
|
101
|
+
const configuredNames = capabilities.map((item) => String(item?.name ?? ''));
|
|
102
|
+
const offered = capabilities.filter((item) => servedNames.includes(String(item?.name ?? '')));
|
|
103
|
+
provider.lastDiscovery = {
|
|
104
|
+
served: servedNames,
|
|
105
|
+
configured: configuredNames,
|
|
106
|
+
missing: configuredNames.filter((name) => !servedNames.includes(name)),
|
|
107
|
+
extra: servedNames.filter((name) => !configuredNames.includes(name)),
|
|
108
|
+
};
|
|
109
|
+
return offered;
|
|
84
110
|
},
|
|
85
111
|
async execute(request = {}) {
|
|
86
112
|
const accepted = await httpJson('/runs', {
|
|
@@ -165,4 +191,5 @@ export function createDeepAgentsProvider({
|
|
|
165
191
|
return () => controller.abort();
|
|
166
192
|
},
|
|
167
193
|
};
|
|
194
|
+
return provider;
|
|
168
195
|
}
|
|
@@ -74,16 +74,44 @@ test('discoverCapabilities fetches /capabilities', async () => {
|
|
|
74
74
|
assert.deepEqual(await provider.discoverCapabilities(), [{ name: 'agent.review', operations: ['run'] }]);
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
-
test('discoverCapabilities
|
|
78
|
-
const fetchImpl = mockFetch({
|
|
77
|
+
test('discoverCapabilities keeps the configured metadata but only for capabilities the gateway serves', async () => {
|
|
78
|
+
const fetchImpl = mockFetch({
|
|
79
|
+
'GET /capabilities': () => jsonResponse(200, [
|
|
80
|
+
{ name: 'agent.review', operations: ['run'] },
|
|
81
|
+
{ name: 'agent.notify', operations: ['run'] },
|
|
82
|
+
]),
|
|
83
|
+
});
|
|
79
84
|
const provider = createDeepAgentsProvider({
|
|
80
85
|
endpoint: 'http://agent-runtime:8080',
|
|
81
|
-
capabilities: [
|
|
86
|
+
capabilities: [
|
|
87
|
+
{ name: 'agent.review', operations: ['run'], aliases: ['audit'] },
|
|
88
|
+
{ name: 'agent.research', operations: ['run'], mutationClass: 'ingest' },
|
|
89
|
+
{ name: 'agent.notify', operations: ['run'], defaultRequiresApproval: true },
|
|
90
|
+
],
|
|
82
91
|
fetchImpl,
|
|
83
92
|
});
|
|
84
93
|
|
|
94
|
+
const offered = await provider.discoverCapabilities();
|
|
95
|
+
assert.equal(fetchImpl.calls.length, 1, 'the gateway is always asked what it serves');
|
|
96
|
+
assert.deepEqual(offered.map((item) => item.name), ['agent.review', 'agent.notify']);
|
|
97
|
+
assert.equal(offered[0].aliases[0], 'audit', 'the configured metadata is what the resolver routes on');
|
|
98
|
+
assert.deepEqual(provider.lastDiscovery, {
|
|
99
|
+
served: ['agent.review', 'agent.notify'],
|
|
100
|
+
configured: ['agent.review', 'agent.research', 'agent.notify'],
|
|
101
|
+
missing: ['agent.research'],
|
|
102
|
+
extra: [],
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('discoverCapabilities with no static config serves the gateway list as-is', async () => {
|
|
107
|
+
const fetchImpl = mockFetch({
|
|
108
|
+
'GET /capabilities': () => jsonResponse(200, [{ name: 'agent.review', operations: ['run'] }]),
|
|
109
|
+
});
|
|
110
|
+
const provider = createDeepAgentsProvider({ endpoint: 'http://agent-runtime:8080', fetchImpl });
|
|
111
|
+
|
|
85
112
|
assert.deepEqual(await provider.discoverCapabilities(), [{ name: 'agent.review', operations: ['run'] }]);
|
|
86
|
-
assert.
|
|
113
|
+
assert.deepEqual(provider.lastDiscovery.missing, []);
|
|
114
|
+
assert.equal(provider.lastDiscovery.configured, null);
|
|
87
115
|
});
|
|
88
116
|
|
|
89
117
|
test('execute POSTs /runs and returns the runId', async () => {
|
|
@@ -24,6 +24,11 @@ export async function discoverRuntimeProviderAgents(runtimeProviders) {
|
|
|
24
24
|
: (runtimeProviders?.list?.() ?? []);
|
|
25
25
|
const agents = [];
|
|
26
26
|
const unavailable = [];
|
|
27
|
+
// Configured-but-not-served capabilities, per runtime (deepagents providers
|
|
28
|
+
// fill `lastDiscovery`). A drift is not an outage: the runtime stays
|
|
29
|
+
// available with the capabilities it really serves, and the difference is
|
|
30
|
+
// reported so an operator can see WHY agent.research is not routable.
|
|
31
|
+
const drift = [];
|
|
27
32
|
|
|
28
33
|
for (const entry of providers) {
|
|
29
34
|
const provider = entry?.provider ?? entry;
|
|
@@ -53,9 +58,13 @@ export async function discoverRuntimeProviderAgents(runtimeProviders) {
|
|
|
53
58
|
for (const capability of capabilities ?? []) {
|
|
54
59
|
agents.push(runtimeProviderAgent(runtimeId, provider, description, capability, health));
|
|
55
60
|
}
|
|
61
|
+
const discovery = provider?.lastDiscovery;
|
|
62
|
+
if (discovery && Array.isArray(discovery.missing) && discovery.missing.length > 0) {
|
|
63
|
+
drift.push({ runtimeId, missing: [...discovery.missing], served: [...(discovery.served ?? [])] });
|
|
64
|
+
}
|
|
56
65
|
}
|
|
57
66
|
|
|
58
|
-
return { agents, unavailable };
|
|
67
|
+
return { agents, unavailable, drift };
|
|
59
68
|
}
|
|
60
69
|
|
|
61
70
|
/**
|
|
@@ -254,7 +263,10 @@ export async function discoverRuntimeProvidersOnce(session, {
|
|
|
254
263
|
},
|
|
255
264
|
});
|
|
256
265
|
const resolved = resolveRuntimeProviders(entries);
|
|
257
|
-
const { agents, unavailable } = await discoverRuntimeProviderAgents(resolved.providers);
|
|
266
|
+
const { agents, unavailable, drift } = await discoverRuntimeProviderAgents(resolved.providers);
|
|
267
|
+
// Exposed for /status: which configured capabilities the runtime does not
|
|
268
|
+
// serve right now. Replaced on every scan — a recovered gateway clears it.
|
|
269
|
+
session.runtimeProviderDrift = drift;
|
|
258
270
|
|
|
259
271
|
// A failed probe is not a lost agent (same rule as agentRegistry): a runtime
|
|
260
272
|
// that reported unavailable this round but for which we hold a last-known-good
|
|
@@ -330,6 +342,20 @@ export async function discoverRuntimeProvidersOnce(session, {
|
|
|
330
342
|
: `agent-runtimes: ${item.runtimeId} unavailable (${item.error})`);
|
|
331
343
|
}
|
|
332
344
|
}
|
|
345
|
+
// Same edge-triggered rule for a capability drift: announced when it
|
|
346
|
+
// appears (or changes), silent while it persists, announced again after a
|
|
347
|
+
// recovery.
|
|
348
|
+
session._runtimeProviderDriftAnnounced ??= new Map();
|
|
349
|
+
const announced = session._runtimeProviderDriftAnnounced;
|
|
350
|
+
const currentDrift = new Map(drift.map((item) => [item.runtimeId, item.missing.join(',')]));
|
|
351
|
+
for (const item of drift) {
|
|
352
|
+
if (announced.get(item.runtimeId) === currentDrift.get(item.runtimeId)) continue;
|
|
353
|
+
dispatchRuntimeLog(session, `agent-runtimes: ${item.runtimeId} does not serve ${item.missing.length} configured capabilit${item.missing.length === 1 ? 'y' : 'ies'} (${item.missing.join(', ')}) — it serves ${item.served.join(', ') || 'nothing'}; check the gateway's /config mount (agent-runtimes.json)`);
|
|
354
|
+
}
|
|
355
|
+
for (const id of [...announced.keys()]) {
|
|
356
|
+
if (!currentDrift.has(id)) announced.delete(id);
|
|
357
|
+
}
|
|
358
|
+
for (const [id, key] of currentDrift) announced.set(id, key);
|
|
333
359
|
session._runtimeProviderDown = currentDown;
|
|
334
360
|
return effectiveAgents;
|
|
335
361
|
}
|
|
@@ -265,6 +265,54 @@ test('discoverRuntimeProvidersOnce announces a down runtime only once across re-
|
|
|
265
265
|
assert.equal(logs.length, 1, 'the degradation is announced once, not on every re-scan');
|
|
266
266
|
});
|
|
267
267
|
|
|
268
|
+
test('a gateway serving fewer capabilities than configured is a drift: offered = intersection, announced once, cleared on recovery', async () => {
|
|
269
|
+
const session = sessionWithEvents();
|
|
270
|
+
const config = [{
|
|
271
|
+
id: 'deepagents', type: 'deepagents', endpoint: 'http://127.0.0.1:7789', enabled: true,
|
|
272
|
+
capabilities: [
|
|
273
|
+
{ name: 'agent.review', operations: ['run'] },
|
|
274
|
+
{ name: 'agent.research', operations: ['run'], mutationClass: 'ingest' },
|
|
275
|
+
{ name: 'agent.notify', operations: ['run'], defaultRequiresApproval: true },
|
|
276
|
+
],
|
|
277
|
+
}];
|
|
278
|
+
const originalFetch = globalThis.fetch;
|
|
279
|
+
let served = [{ name: 'agent.review', operations: ['run'] }];
|
|
280
|
+
globalThis.fetch = async (url) => {
|
|
281
|
+
if (String(url).includes('/health')) return new Response(JSON.stringify({ ok: true, version: 't' }), { status: 200, headers: { 'content-type': 'application/json' } });
|
|
282
|
+
if (String(url).includes('/capabilities')) return new Response(JSON.stringify(served), { status: 200, headers: { 'content-type': 'application/json' } });
|
|
283
|
+
throw new Error('unexpected probe ' + String(url));
|
|
284
|
+
};
|
|
285
|
+
try {
|
|
286
|
+
await discoverRuntimeProvidersOnce(session, { config });
|
|
287
|
+
await discoverRuntimeProvidersOnce(session, { config });
|
|
288
|
+
const driftLogs = () => (session.agentEvents ?? []).filter((event) => event.type === 'runtime_log'
|
|
289
|
+
&& String(event.payload?.message ?? '').includes('does not serve'));
|
|
290
|
+
|
|
291
|
+
// Only what the gateway serves is routable — a governed capability the
|
|
292
|
+
// gateway would run ungoverned is not offered at all.
|
|
293
|
+
assert.deepEqual(session.runtimeProviderAgents.map((agent) => agent.agentInstanceId), ['deepagents::agent.review']);
|
|
294
|
+
assert.deepEqual(session.runtimeProviderDrift, [{
|
|
295
|
+
runtimeId: 'deepagents', missing: ['agent.research', 'agent.notify'], served: ['agent.review'],
|
|
296
|
+
}]);
|
|
297
|
+
assert.equal(driftLogs().length, 1, 'announced once across re-scans');
|
|
298
|
+
assert.match(String(driftLogs()[0].payload.message), /does not serve 2 configured capabilities \(agent\.research, agent\.notify\)/);
|
|
299
|
+
assert.match(String(driftLogs()[0].payload.message), /\/config mount/);
|
|
300
|
+
|
|
301
|
+
// The gateway recovers its config: no drift, all three routable.
|
|
302
|
+
served = config[0].capabilities.map(({ name, operations }) => ({ name, operations }));
|
|
303
|
+
await discoverRuntimeProvidersOnce(session, { config });
|
|
304
|
+
assert.equal(session.runtimeProviderAgents.length, 3);
|
|
305
|
+
assert.deepEqual(session.runtimeProviderDrift, []);
|
|
306
|
+
|
|
307
|
+
// …and a later drift is announced again.
|
|
308
|
+
served = [{ name: 'agent.review', operations: ['run'] }];
|
|
309
|
+
await discoverRuntimeProvidersOnce(session, { config });
|
|
310
|
+
assert.equal(driftLogs().length, 2);
|
|
311
|
+
} finally {
|
|
312
|
+
globalThis.fetch = originalFetch;
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
|
|
268
316
|
test('an HTTP 401 gateway probe is announced as not-ready with the ⚠ glyph, not as a failure', async () => {
|
|
269
317
|
const session = sessionWithEvents();
|
|
270
318
|
const originalFetch = globalThis.fetch;
|