@vostride/agent-qa-mcp 0.1.21 → 0.2.1

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/index.js CHANGED
@@ -1,1124 +1,2201 @@
1
- // src/server.ts
2
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
- import {
4
- buildAnalyticsEvent as buildAnalyticsEvent2,
5
- captureAnalytics as captureAnalytics2,
6
- resolveAnalyticsStandardProperties as resolveAnalyticsStandardProperties2
7
- } from "@vostride/agent-qa-core";
1
+ // src/catalog.ts
2
+ import { McpServer, ProtocolError, ProtocolErrorCode } from "@modelcontextprotocol/server";
3
+ import { invokeFunction as invokeFunction3, RUNS_ARTIFACT_FUNCTION_ID } from "@vostride/agent-qa-agent";
8
4
 
9
- // src/agent-qa-server.ts
10
- import { readFile } from "fs/promises";
11
- import { resolve } from "path";
12
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13
- import {
14
- AgentQaConfigSchema as AgentQaConfigSchema2,
15
- buildAnalyticsEvent,
16
- captureAnalytics,
17
- createAnalyticsService,
18
- getAgentQaVersion,
19
- redactAuthStateValue,
20
- resolveAnalyticsStandardProperties
21
- } from "@vostride/agent-qa-core";
5
+ // src/app-resource.ts
6
+ import { CLIENT_CAPABILITIES_META_KEY, ResourceTemplate } from "@modelcontextprotocol/server";
22
7
  import {
23
- ENTITY_ID_TYPES,
24
- generateCanonicalId,
25
- getEntityIdContract,
26
- getEntityIdContracts as getEntityIdContracts2,
27
- isCanonicalId
28
- } from "@vostride/agent-qa-ids";
29
- import { parse as parseYaml } from "yaml";
30
- import { z } from "zod";
8
+ invokeFunction,
9
+ FunctionNotBoundError,
10
+ LiveViewUnavailableError,
11
+ RUNS_EVENTS_FUNCTION_ID,
12
+ RUNS_LIVE_URL_FUNCTION_ID,
13
+ UnknownRunError
14
+ } from "@vostride/agent-qa-agent";
15
+ import { handleRosterOf, RunsLiveUrlInputSchema } from "@vostride/agent-qa-schema";
31
16
 
32
- // src/schema-reference.ts
33
- import {
34
- AgentQaConfigSchema,
35
- HooksFileSchema,
36
- SuiteDefinitionSchema,
37
- TestDefinitionSchema
38
- } from "@vostride/agent-qa-core";
39
- import {
40
- getEntityIdContracts
41
- } from "@vostride/agent-qa-ids";
42
- var SCHEMA_REFERENCE_NAMES = ["config", "test", "suite", "hooks", "ids"];
43
- var VALIDATION_KINDS = ["config", "test", "suite", "hooks"];
44
- var AGENT_QA_SCHEMA_REFERENCES = {
45
- config: {
46
- name: "agent-qa config",
47
- rootKeys: ["workspace", "services", "registry", "use", "analytics"],
48
- services: {
49
- dashboard: ["port", "dbPath", "artifactsDir"],
50
- mcp: ["enabled", "transport", "host", "port", "path"],
51
- cache: ["dir", "ttl"],
52
- logging: ["level"],
53
- recording: ["enabled"],
54
- accessibility: ["enabled", "standard", "runAfter", "failOnViolation", "disableRules", "exclude"],
55
- memory: ["enabled", "provider", "dir", "minTrust", "maxInjections", "curatorEnabled"]
56
- },
57
- mcpLocalOnly: {
58
- transports: ["http", "stdio"],
59
- hosts: ["localhost", "127.0.0.1", "::1"],
60
- defaultPort: 3471,
61
- defaultPath: "/mcp"
62
- }
63
- },
64
- test: {
65
- name: "agent-qa test YAML",
66
- requiredKeys: ["test-id", "name", "target", "steps"],
67
- optionalKeys: ["context", "use", "meta", "setup", "teardown"],
68
- idType: "test",
69
- hookReferences: "setup and teardown arrays must contain canonical h_ hook IDs.",
70
- stepFormats: ["string", "object with step/timeout/retries/screenshot/capture/maxAttempts"]
71
- },
72
- suite: {
73
- name: "agent-qa suite YAML",
74
- requiredKeys: ["name", "target", "tests"],
75
- optionalKeys: ["suite-id", "context", "setup", "teardown", "use"],
76
- idType: "suite",
77
- testReference: "Each tests[] entry uses { test: path, id: canonical t_ test ID }.",
78
- hookReferences: "setup and teardown arrays must contain canonical h_ hook IDs."
79
- },
80
- hooks: {
81
- name: "agent-qa hooks file",
82
- rootKeys: ["hooks"],
83
- hookKeys: ["id", "name", "runtime", "file", "deps", "packageFile", "timeout", "network"],
84
- idType: "hook",
85
- runtimes: ["node", "bun", "python", "bash"]
86
- },
87
- ids: {
88
- name: "agent-qa canonical IDs",
89
- instruction: "Generate IDs with agent_qa_generate_id or `agent-qa ids generate <type>`; never hand-write IDs.",
90
- contracts: getEntityIdContracts()
91
- }
17
+ // src/app-wrapper.ts
18
+ var APP_WRAPPER_STATES = [
19
+ "connecting",
20
+ "live",
21
+ "expired",
22
+ "unreachable-origin",
23
+ "run-ended"
24
+ ];
25
+ var APP_WRAPPER_CONNECTING_MESSAGE = "connecting to the live view\u2026";
26
+ var APP_WRAPPER_CONNECTING_HINT = "if the pane does not appear, open it in your browser";
27
+ var APP_WRAPPER_LIVE_MESSAGE = "live";
28
+ var APP_WRAPPER_EXPIRED_MESSAGE = "live link expired";
29
+ var APP_WRAPPER_EXPIRED_HINT = "ask to watch again \u2014 a fresh watch mints a new link";
30
+ var APP_WRAPPER_UNREACHABLE_MESSAGE = "this host can't reach the live view \u2014 open it in your browser";
31
+ var APP_WRAPPER_RUN_ENDED_MESSAGE = "this run is not live \u2014 open it in your browser";
32
+ var APP_WRAPPER_PERMANENT_LINK_LABEL = "open in your browser";
33
+ var APP_WRAPPER_LOAD_TIMEOUT_MS = 1e4;
34
+ var APP_WRAPPER_UI_PROTOCOL_VERSION = "2026-01-26";
35
+ var APP_WRAPPER_APP_NAME = "agent-qa-live-view";
36
+ var APP_WRAPPER_APP_VERSION = "1.0.0";
37
+ var STYLE = `
38
+ :root { color-scheme: dark light }
39
+ html, body { margin: 0; height: 100%; background: #0b0d10; }
40
+ main { display: grid; grid-template-rows: auto 1fr auto auto; height: 100%; font: 14px/1.5 ui-sans-serif, system-ui, sans-serif; color: #c9d1d9; }
41
+ header { display: flex; gap: .75rem; align-items: center; padding: .5rem .75rem; }
42
+ header:empty { display: none; }
43
+ iframe { border: 0; width: 100%; height: 100%; display: none; background: #0b0d10; }
44
+ main[data-app-state="live"] iframe { display: block; }
45
+ main[data-app-state="live"] p[data-app-message] { display: none; }
46
+ main[data-app-state="live"] p[data-app-hint] { display: none; }
47
+ p[data-app-message] { margin: 0; padding: .5rem .75rem; }
48
+ p[data-app-hint] { margin: 0; padding: 0 .75rem .5rem; opacity: .75; }
49
+ p[data-app-hint]:empty { display: none; }
50
+ footer { padding: .5rem .75rem; }
51
+ select[hidden], a[hidden] { display: none; }
52
+ `;
53
+ var MESSAGES = {
54
+ connecting: APP_WRAPPER_CONNECTING_MESSAGE,
55
+ live: APP_WRAPPER_LIVE_MESSAGE,
56
+ expired: APP_WRAPPER_EXPIRED_MESSAGE,
57
+ "unreachable-origin": APP_WRAPPER_UNREACHABLE_MESSAGE,
58
+ "run-ended": APP_WRAPPER_RUN_ENDED_MESSAGE
92
59
  };
93
- var VALIDATION_SCHEMAS = {
94
- config: AgentQaConfigSchema,
95
- test: TestDefinitionSchema,
96
- suite: SuiteDefinitionSchema,
97
- hooks: HooksFileSchema
60
+ var HINTS = {
61
+ connecting: APP_WRAPPER_CONNECTING_HINT,
62
+ live: "",
63
+ expired: APP_WRAPPER_EXPIRED_HINT,
64
+ "unreachable-origin": "",
65
+ "run-ended": ""
98
66
  };
99
- function validateAgentQaDefinition(kind, definition) {
100
- const result = VALIDATION_SCHEMAS[kind].safeParse(definition);
101
- if (result.success) {
102
- return {
103
- valid: true,
104
- kind,
105
- issues: []
106
- };
67
+ function httpPermanentLink(value) {
68
+ if (typeof value !== "string") return void 0;
69
+ let parsed;
70
+ try {
71
+ parsed = new URL(value);
72
+ } catch {
73
+ return void 0;
107
74
  }
108
- return {
109
- valid: false,
110
- kind,
111
- issues: result.error.issues.map((issue) => ({
112
- path: issue.path.map(String).join(".") || "(root)",
113
- message: issue.message
114
- }))
75
+ return parsed.protocol === "http:" || parsed.protocol === "https:" ? value : void 0;
76
+ }
77
+ function renderAppWrapperPage(input) {
78
+ const scriptLiteral = (value) => JSON.stringify(value).replaceAll("<", "\\u003c");
79
+ const attributeText = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
80
+ const now = input.now ?? Date.now();
81
+ const live = input.runLive && input.mints.length > 0;
82
+ const mints = live ? input.mints : [];
83
+ const lowestExpiry = mints.reduce(
84
+ (lowest, mint) => mint.expiresAt < lowest ? mint.expiresAt : lowest,
85
+ Number.POSITIVE_INFINITY
86
+ );
87
+ const expired = live && lowestExpiry <= now;
88
+ const expiresInMs = expired || !live ? 0 : Math.max(0, lowestExpiry - now);
89
+ const initialState = !live ? "run-ended" : expired ? "expired" : "connecting";
90
+ const mintsLiteral = (expired ? [] : mints).map(
91
+ (mint) => `{handle:${scriptLiteral(String(mint.handle))},url:${scriptLiteral(mint.liveUrl)},kind:${scriptLiteral(mint.streamKind)}}`
92
+ ).join(",");
93
+ const permanentLink = httpPermanentLink(input.permanentLink);
94
+ const permanentLinkSlot = permanentLink === void 0 ? "" : '<a data-app-permanent-link rel="noreferrer" target="_blank" href="' + attributeText(permanentLink) + '">' + attributeText(APP_WRAPPER_PERMANENT_LINK_LABEL) + "</a>";
95
+ return `<!DOCTYPE html>
96
+ <html lang="en">
97
+ <head>
98
+ <meta charset="utf-8">
99
+ <meta name="viewport" content="width=device-width, initial-scale=1">
100
+ <title>agent-qa live run</title>
101
+ <style>${STYLE}</style>
102
+ </head>
103
+ <body>
104
+ <main data-app-state="${initialState}">
105
+ <header><select data-app-handle-picker hidden></select></header>
106
+ ${live && !expired ? '<iframe data-app-live-frame sandbox="allow-scripts" referrerpolicy="no-referrer" title="agent-qa live view"></iframe>' : ""}
107
+ <p data-app-message>${MESSAGES[initialState]}</p>
108
+ <p data-app-hint>${HINTS[initialState]}</p>
109
+ <footer>${permanentLinkSlot}</footer>
110
+ </main>
111
+ <script>
112
+ (function () {
113
+ var mints = [${mintsLiteral}];
114
+ var runId = ${scriptLiteral(input.runId)};
115
+ var expiresInMs = ${String(expiresInMs)};
116
+ var loadTimeoutMs = ${String(APP_WRAPPER_LOAD_TIMEOUT_MS)};
117
+ var messages = {
118
+ connecting: ${scriptLiteral(APP_WRAPPER_CONNECTING_MESSAGE)},
119
+ live: ${scriptLiteral(APP_WRAPPER_LIVE_MESSAGE)},
120
+ expired: ${scriptLiteral(APP_WRAPPER_EXPIRED_MESSAGE)},
121
+ 'unreachable-origin': ${scriptLiteral(APP_WRAPPER_UNREACHABLE_MESSAGE)},
122
+ 'run-ended': ${scriptLiteral(APP_WRAPPER_RUN_ENDED_MESSAGE)}
123
+ };
124
+ var hints = {
125
+ connecting: ${scriptLiteral(APP_WRAPPER_CONNECTING_HINT)},
126
+ live: '',
127
+ expired: ${scriptLiteral(APP_WRAPPER_EXPIRED_HINT)},
128
+ 'unreachable-origin': '',
129
+ 'run-ended': ''
115
130
  };
131
+
132
+ var root = document.querySelector('[data-app-state]');
133
+ var frame = document.querySelector('[data-app-live-frame]');
134
+ var picker = document.querySelector('[data-app-handle-picker]');
135
+ var message = document.querySelector('[data-app-message]');
136
+ var hint = document.querySelector('[data-app-hint]');
137
+
138
+ // WHICH RUN, as an attribute rather than as prose. Identity is not run STATE (D-11 forbids the
139
+ // second, not the first), and it enters through setAttribute from an escaped literal, so a run
140
+ // id can never become markup no matter what it spells.
141
+ root.setAttribute('data-app-run-id', runId);
142
+
143
+ // The state machine is an ATTRIBUTE plus two textContent writes, exactly as the embed page's is:
144
+ // a state a person can read off the DOM is a state a cell can assert without screenshotting, and
145
+ // text that arrives through textContent can never become markup.
146
+ function setState(state) {
147
+ root.setAttribute('data-app-state', state);
148
+ message.textContent = messages[state] || '';
149
+ hint.textContent = hints[state] || '';
150
+ }
151
+
152
+ // ---- The minimal ui/initialize handshake (ext-apps spec shape) -------------------------------
153
+ //
154
+ // HAND-ROLLED AND DELIBERATELY ISOLATED IN THIS ONE BLOCK. The ext-apps package
155
+ // declares a peer on the v1 monolith SDK, so importing its app-bridge into production would drag
156
+ // a second SDK generation in (Pitfall 3) \u2014 and this page has no bundler anyway. RESEARCH
157
+ // assumption A5 rated the method-name and field spellings MEDIUM (single spec fetch), and plan
158
+ // 02's coverage entry D9 deferred the adjudication to plan 06.
159
+ //
160
+ // PLAN 06 MEASURED IT, AND IT HAD DRIFTED. Against the installed
161
+ // @modelcontextprotocol/ext-apps@1.7.5's own generated schema, McpUiInitializeRequest.params
162
+ // requires THREE members: protocolVersion, appInfo and appCapabilities. This block sent only the
163
+ // first, so a host validating the request against the published schema would have rejected the
164
+ // handshake outright, and the failure would have looked like a host that simply does not support
165
+ // Apps. The two missing members are now sent, and ext-apps-constants.test.ts cell (4d) derives
166
+ // the required list FROM the shipped schema rather than restating it, so the next spec change
167
+ // reds here instead of on a user's machine.
168
+ //
169
+ // IT STILL ASKS FOR NOTHING. appCapabilities is deliberately empty: this document exposes no
170
+ // tools, requests no display mode and declares no experimental feature (the schema requires no
171
+ // member of it). No permissions, no re-mint (D-10) \u2014 it announces that this document is ready,
172
+ // which is all a wrapper that renders no run state of its own has to say.
173
+ try {
174
+ if (window.parent !== window) {
175
+ window.parent.postMessage({
176
+ jsonrpc: '2.0',
177
+ id: 'ui-init-1',
178
+ method: 'ui/initialize',
179
+ params: {
180
+ protocolVersion: ${scriptLiteral(APP_WRAPPER_UI_PROTOCOL_VERSION)},
181
+ appInfo: { name: ${scriptLiteral(APP_WRAPPER_APP_NAME)}, version: ${scriptLiteral(APP_WRAPPER_APP_VERSION)} },
182
+ appCapabilities: {}
183
+ }
184
+ }, '*');
185
+ }
186
+ } catch (e) { /* a host that refuses the message changes nothing about what this page shows */ }
187
+
188
+ if (mints.length === 0) {
189
+ // Either D-07's honest terminal/unknown state or D-10's expired one \u2014 both were decided by the
190
+ // RENDERER and are already in the markup. No src was ever assigned, so there is no blank frame
191
+ // to mistake for a stalled one, and nothing below this line can move the document off them.
192
+ return;
193
+ }
194
+
195
+ // ---- Reachability, once per ORIGIN (see the header's measured note) ---------------------------
196
+ //
197
+ // Memoised deliberately, and ONLY ON SUCCESS. The picker swaps among panes that share one
198
+ // origin, so after the first ANSWERING pane every swap is served from this map and issues NO
199
+ // network request at all (D-06/D-10's zero re-mints, zero calls, which the DOM cell counts) \u2014 a
200
+ // property that rides entirely on caching the true. The catch arm therefore FORGETS the entry
201
+ // before answering false: a refusal is a fact about one moment, not about the origin, and every
202
+ // mint on a render shares one origin, so a cached false was total and permanent. See the header.
203
+ var reach = {};
204
+ function reachable(url) {
205
+ var origin;
206
+ try { origin = new URL(url).origin; } catch (e) { return Promise.resolve(false); }
207
+ if (reach[origin] === undefined) {
208
+ reach[origin] = fetch(origin + '/', { mode: 'no-cors', cache: 'no-store' })
209
+ .then(function () { return true; })
210
+ .catch(function () { delete reach[origin]; return false; });
211
+ }
212
+ return reach[origin];
213
+ }
214
+
215
+ var attachId = 0;
216
+
217
+ function attach(mint) {
218
+ // THE EXPIRED STATE IS TERMINAL, AND THIS IS THE ONE PLACE IT COULD BE LEFT (D-10). The
219
+ // picker's change handler \u2014 and nothing else \u2014 can reach this function after the one-shot
220
+ // expiry timer has fired; without this line the next statements would set the connecting state
221
+ // and assign an aged token over a document that had already told the truth, and no timer would
222
+ // ever correct it. The identical predicate inside settle() stays where it is: it closes the
223
+ // OTHER door, a probe or a frame load that was already in flight when the timer fired, so
224
+ // neither guard makes the other dead.
225
+ if (root.getAttribute('data-app-state') === 'expired') return;
226
+ var mine = (attachId += 1);
227
+ var loaded = false;
228
+ var settled = false;
229
+
230
+ function settle(state) {
231
+ if (settled || mine !== attachId) return;
232
+ if (root.getAttribute('data-app-state') === 'expired') return;
233
+ settled = true;
234
+ setState(state);
235
+ }
236
+
237
+ setState('connecting');
238
+ // THE ONLY PLACE A TOKEN IS WRITTEN. It goes into the frame's src and nowhere else.
239
+ frame.src = mint.url;
240
+
241
+ frame.addEventListener('error', function () { settle('unreachable-origin'); }, { once: true });
242
+ frame.addEventListener('load', function () {
243
+ // NECESSARY, NOT SUFFICIENT: Chromium fires this for a refused connection too (header note),
244
+ // so the probe is what decides \u2014 a frame that loaded on an origin nothing answered for is an
245
+ // error page, and calling that 'live' is the failure this arm exists to prevent.
246
+ loaded = true;
247
+ reachable(mint.url).then(function (ok) { settle(ok ? 'live' : 'unreachable-origin'); });
248
+ }, { once: true });
249
+
250
+ reachable(mint.url).then(function (ok) {
251
+ if (!ok) settle('unreachable-origin');
252
+ else if (loaded) settle('live');
253
+ });
254
+
255
+ // D-11's REQUIRED arm for the origin that never answers at all: a black-holed host fires no
256
+ // error and never loads, so silence past this bound is itself the signal, named rather than
257
+ // left as a spinner.
258
+ setTimeout(function () { settle('unreachable-origin'); }, loadTimeoutMs);
259
+ }
260
+
261
+ // D-10: the deadline was computed at RENDER time and arrives as a delta, so the host's own clock
262
+ // never enters the comparison. When it fires the document says so and stops \u2014 nothing here mints.
263
+ if (expiresInMs > 0) {
264
+ setTimeout(function () {
265
+ attachId += 1;
266
+ // Unconditional: the script returned above when mints was empty, and a non-empty mints is
267
+ // exactly the condition under which the renderer emits the iframe, so there is no reachable
268
+ // state in which this callback runs without a frame to strip.
269
+ frame.removeAttribute('src');
270
+ // The picker is an affordance of the EMBEDDED variant; when the variant is gone the
271
+ // affordance goes with it, so the control a person can reach and the state the document is in
272
+ // cannot disagree.
273
+ picker.setAttribute('disabled', '');
274
+ setState('expired');
275
+ }, expiresInMs);
276
+ }
277
+
278
+ // D-06/D-10: the picker swaps among tokens ALREADY minted for this render. It never mints, and
279
+ // there is no bridge here that could ask anyone to \u2014 a fresh watch call is what re-mints.
280
+ for (var i = 0; i < mints.length; i += 1) {
281
+ var option = document.createElement('option');
282
+ option.value = String(i);
283
+ option.textContent = 'handle ' + mints[i].handle;
284
+ picker.appendChild(option);
285
+ }
286
+ if (mints.length > 1) picker.removeAttribute('hidden');
287
+ picker.addEventListener('change', function () {
288
+ var chosen = mints[Number(picker.value)];
289
+ if (chosen !== undefined) attach(chosen);
290
+ });
291
+
292
+ attach(mints[0]);
293
+ }());
294
+ </script>
295
+ </body>
296
+ </html>
297
+ `;
116
298
  }
117
299
 
118
- // src/agent-qa-server.ts
119
- var DISCOVERY = {
120
- name: "agent-qa",
121
- purpose: "Author, validate, run, and triage agent-qa tests through local trusted MCP.",
122
- tools: [
123
- "agent_qa_discover",
124
- "agent_qa_get_config",
125
- "agent_qa_schema_reference",
126
- "agent_qa_validate_definition",
127
- "agent_qa_generate_id",
128
- "agent_qa_validate_id",
129
- "agent_qa_list_tests",
130
- "agent_qa_read_test",
131
- "agent_qa_validate_test",
132
- "agent_qa_create_test",
133
- "agent_qa_update_test",
134
- "agent_qa_delete_test",
135
- "agent_qa_list_suites",
136
- "agent_qa_read_suite",
137
- "agent_qa_validate_suite",
138
- "agent_qa_create_suite",
139
- "agent_qa_update_suite",
140
- "agent_qa_delete_suite",
141
- "agent_qa_list_hooks",
142
- "agent_qa_read_hook",
143
- "agent_qa_create_hook",
144
- "agent_qa_update_hook",
145
- "agent_qa_delete_hook",
146
- "agent_qa_run_hook",
147
- "agent_qa_enqueue_test_run",
148
- "agent_qa_enqueue_suite_run",
149
- "agent_qa_get_run",
150
- "agent_qa_get_run_steps",
151
- "agent_qa_get_run_logs",
152
- "agent_qa_get_run_execution_logs",
153
- "agent_qa_get_run_artifact",
154
- "agent_qa_cancel_run",
155
- "agent_qa_classify_failure"
156
- ],
157
- resources: SCHEMA_REFERENCE_NAMES.map((name) => `agent-qa://schema/${name}`),
158
- prompts: ["agent_qa_authoring_context"],
159
- phases: {
160
- current: "discovery-schema-id-contracts",
161
- next: ["authoring tools", "run execution and triage tools"]
300
+ // src/live-link.ts
301
+ function liveRunLinkOf(base, runId) {
302
+ return `${base.replace(/\/+$/u, "")}/runs/${encodeURIComponent(runId)}/live`;
303
+ }
304
+ function liveLinkLine(url) {
305
+ return `watch live or review: ${url}`;
306
+ }
307
+ var LIVE_LINK_ABSENT_LINE = "no dashboard configured \u2014 set services.dashboard.baseUrl";
308
+ var LIVE_LINK_NO_RUN_LINE = "no run to link to \u2014 this result names no run id";
309
+ function liveLinkLineFor(base, runId) {
310
+ if (base === void 0 || base.trim() === "") return LIVE_LINK_ABSENT_LINE;
311
+ if (runId === void 0 || runId === "") return LIVE_LINK_NO_RUN_LINE;
312
+ return liveLinkLine(liveRunLinkOf(base.trim(), runId));
313
+ }
314
+
315
+ // src/app-resource.ts
316
+ var MCP_APPS_EXTENSION_ID = "io.modelcontextprotocol/ui";
317
+ var APP_RESOURCE_MIME_TYPE = "text/html;profile=mcp-app";
318
+ var APP_RESOURCE_URI_TEMPLATE = "ui://agent-qa/live-run/{runId}";
319
+ function appResourceUriFor(runId) {
320
+ return APP_RESOURCE_URI_TEMPLATE.replace("{runId}", encodeURIComponent(runId));
321
+ }
322
+ var APP_RESOURCE_NAME = "agent_qa_live_run_app";
323
+ var APP_RESOURCE_TITLE = "agent-qa live run";
324
+ var APP_RESOURCE_DESCRIPTION = "The live view for one run, as an MCP App: a sandboxed page embedding the run's live preview for each member handle. Rendered fresh on every read.";
325
+ var APP_URI_PREFIX = "ui://agent-qa/live-run/";
326
+ function parseAppUri(href) {
327
+ if (!href.startsWith(APP_URI_PREFIX)) return void 0;
328
+ const tail = href.slice(APP_URI_PREFIX.length);
329
+ if (tail === "") return void 0;
330
+ let decoded;
331
+ try {
332
+ decoded = decodeURIComponent(tail);
333
+ } catch {
334
+ return void 0;
335
+ }
336
+ const parsed = RunsLiveUrlInputSchema.safeParse({ runId: decoded });
337
+ if (!parsed.success) return void 0;
338
+ return { runId: parsed.data.runId };
339
+ }
340
+ var APP_NEGOTIATION_CLIENT_CAPABILITIES_KEY = CLIENT_CAPABILITIES_META_KEY;
341
+ function negotiatedAppsExtension(ctx) {
342
+ const envelope = ctx?.mcpReq?.envelope;
343
+ if (envelope === void 0) return false;
344
+ const capabilities = envelope[APP_NEGOTIATION_CLIENT_CAPABILITIES_KEY];
345
+ const extensions = capabilities?.extensions;
346
+ if (extensions === void 0) return false;
347
+ return Object.hasOwn(extensions, MCP_APPS_EXTENSION_ID);
348
+ }
349
+ var AppResourceNegotiationError = class extends Error {
350
+ constructor() {
351
+ super(
352
+ `this resource is an MCP App: it is served only to a client that declared the ${MCP_APPS_EXTENSION_ID} extension in its capabilities`
353
+ );
354
+ this.name = "AppResourceNegotiationError";
162
355
  }
163
356
  };
164
- function jsonContent(value) {
165
- return {
166
- content: [{
167
- type: "text",
168
- text: JSON.stringify(value, null, 2)
169
- }]
170
- };
357
+ var AppResourceUriError = class extends Error {
358
+ constructor(detail) {
359
+ super(`${detail} is not a canonical ui://agent-qa/live-run address`);
360
+ this.name = "AppResourceUriError";
361
+ }
362
+ };
363
+ var AppResourceReadError = class extends Error {
364
+ constructor(message) {
365
+ super(message);
366
+ this.name = "AppResourceReadError";
367
+ }
368
+ };
369
+ var AppResourceOriginSpanError = class extends Error {
370
+ constructor(origins) {
371
+ super(
372
+ `this App document would span ${origins.length} origins (${[...origins].sort().join(", ")}) and one frame allowance covers one: refusing to serve a document whose panes it could not all reach`
373
+ );
374
+ this.name = "AppResourceOriginSpanError";
375
+ }
376
+ };
377
+ function isNotLiveRefusal(error) {
378
+ return error instanceof UnknownRunError || error instanceof LiveViewUnavailableError || error instanceof FunctionNotBoundError;
171
379
  }
172
- function errorContent(message) {
173
- return {
174
- isError: true,
175
- content: [{
176
- type: "text",
177
- text: message
178
- }]
380
+ async function announcedRoster(invoke, runId) {
381
+ const stream = await invoke(RUNS_EVENTS_FUNCTION_ID, { runId });
382
+ const iterable = stream;
383
+ if (iterable === void 0 || typeof iterable[Symbol.asyncIterator] !== "function") {
384
+ return void 0;
385
+ }
386
+ const iterator = iterable[Symbol.asyncIterator]();
387
+ try {
388
+ const first = await iterator.next();
389
+ if (first.done === true) return void 0;
390
+ return handleRosterOf(first.value);
391
+ } finally {
392
+ await iterator.return?.(void 0);
393
+ }
394
+ }
395
+ function createAppResourceReadHandler(deps) {
396
+ return async (uri, _variables, ctx) => {
397
+ if (!negotiatedAppsExtension(ctx)) throw new AppResourceNegotiationError();
398
+ const parsed = parseAppUri(uri.href);
399
+ if (parsed === void 0) {
400
+ throw new AppResourceUriError(deps.maskText(uri.href));
401
+ }
402
+ const { runId } = parsed;
403
+ let roster;
404
+ let notLive = false;
405
+ try {
406
+ roster = await announcedRoster(deps.invoke, runId);
407
+ } catch (err) {
408
+ if (!isNotLiveRefusal(err)) {
409
+ throw new AppResourceReadError(
410
+ deps.maskText(err instanceof Error ? err.message : String(err))
411
+ );
412
+ }
413
+ notLive = true;
414
+ }
415
+ const rosterToMint = roster === void 0 || roster.length === 0 ? [0] : roster;
416
+ const mints = [];
417
+ if (!notLive) {
418
+ for (const handle of rosterToMint) {
419
+ let minted;
420
+ try {
421
+ minted = await deps.invoke(RUNS_LIVE_URL_FUNCTION_ID, {
422
+ runId,
423
+ handle
424
+ });
425
+ } catch (err) {
426
+ if (!isNotLiveRefusal(err)) {
427
+ throw new AppResourceReadError(
428
+ deps.maskText(err instanceof Error ? err.message : String(err))
429
+ );
430
+ }
431
+ continue;
432
+ }
433
+ mints.push({
434
+ handle: minted.handle,
435
+ liveUrl: minted.liveUrl,
436
+ expiresAt: minted.expiresAt,
437
+ streamKind: minted.stream.kind
438
+ });
439
+ }
440
+ }
441
+ const base = deps.liveLink?.dashboardBaseUrl();
442
+ const permanentLink = base === void 0 || base.trim() === "" ? void 0 : liveRunLinkOf(base.trim(), runId);
443
+ const frameOrigins = [
444
+ ...new Set(
445
+ mints.map((mint) => originOf(mint.liveUrl)).filter((origin) => origin !== void 0)
446
+ )
447
+ ];
448
+ if (frameOrigins.length > 1) throw new AppResourceOriginSpanError(frameOrigins);
449
+ const frameDomain = frameOrigins[0];
450
+ const html = renderAppWrapperPage({
451
+ runId,
452
+ mints,
453
+ permanentLink,
454
+ runLive: mints.length > 0
455
+ });
456
+ return {
457
+ contents: [
458
+ {
459
+ // Re-formatted through the codec's own vocabulary rather than echoed, so the address a
460
+ // caller reads back is the canonical one.
461
+ uri: `${APP_URI_PREFIX}${runId}`,
462
+ mimeType: APP_RESOURCE_MIME_TYPE,
463
+ text: html,
464
+ // The host's default is `frame-src 'none'`, so an App that names no frame domain paints
465
+ // a BLOCKED inner frame — the blank pane criterion 2 forbids (Pitfall 1). The allowed
466
+ // origin is the minted link's OWN origin rather than a separately configured one, so the
467
+ // allow can never be broader than the document needs (T-408-05). `frameDomain` is the ONE
468
+ // distinct origin `frameOrigins` above resolved over EVERY mint (IN-02) — not `mints[0]`'s,
469
+ // which was the same answer only by a fact held in another package.
470
+ //
471
+ // TWO ABSENCES, NOT ONE (corrected 2026-09-01, 408 gap round; `408-VERIFICATION.md` W-4 /
472
+ // `408-REVIEW.md` WR-03). This comment used to read "with no mints there is no inner
473
+ // frame, and the block is omitted rather than emitted empty", which describes only the
474
+ // FIRST of two conditions that both reach `frameDomain === undefined`. They are different
475
+ // questions with different served documents, and collapsing them told a reader about a
476
+ // state the code does not produce:
477
+ // - `mints.length` asks whether there is anything to render at all. Zero mints means
478
+ // `runLive` is `false`, the wrapper emits no `<iframe data-app-live-frame>`, and the
479
+ // omitted `csp` block allows nothing because nothing needs allowing. Honest.
480
+ // - `frameOrigins[0]` is the SEPARATE question of whether any surviving mint yielded a
481
+ // parseable origin to put in `_meta.ui.csp`. With `mints.length > 0` and every
482
+ // `originOf(mint.liveUrl)` answering `undefined`, `runLive` is `true` — the document
483
+ // DOES emit the iframe and DOES assign `frame.src` — while the `csp` block is omitted,
484
+ // so a conformant host falls back to `frame-src 'none'` and the pane is blocked and
485
+ // blank. Not honest, and the sentence above never mentioned it.
486
+ // WHY NO SHIPPED PORT REACHES IT: `runs.liveurl` would have to answer a `liveUrl` that
487
+ // is not a parseable URL, and no shipped port does. The verifier measured this as
488
+ // unreachable and classified W-4 as a documentation finding rather than a gap; the
489
+ // wrapper's probe/timeout arm would in any case settle `unreachable-origin` with the
490
+ // permanent link rather than leave a blank pane under a progress claim, so SC2 survives
491
+ // even on the unreachable path.
492
+ //
493
+ // THE INVARIANT IS UNASSERTED, AND THAT IS A DATED DEFERRAL RATHER THAN A SILENCE
494
+ // (2026-09-01, 408 gap round). "Every mint that survives into `runLive` has a nameable
495
+ // origin" is asserted nowhere today — not here, not in `renderAppWrapperPage`, not in a
496
+ // cell. It is the natural candidate and it is NOT taken in this round, because this round
497
+ // is a gap-closure round whose whole reported gap is documentation integrity.
498
+ // THE RULE THAT MAKES THAT CONSISTENT, because the neighbouring round looks
499
+ // contradictory and a reader is entitled to it: THIS ROUND CHANGES WHAT THE SYSTEM
500
+ // SAYS, NEVER WHAT IT DOES. 408-16 task 1 does edit a runtime file — `config.ts` — but
501
+ // it only adds words to an error that was already being thrown on exactly the inputs it
502
+ // was already thrown on; no parse outcome moves in either direction and no input that
503
+ // succeeded now fails. Asserting the invariant here would add a NEW failure mode to a
504
+ // path that currently succeeds. Operator-facing error prose is in scope because it is
505
+ // prose; a new runtime invariant is out of scope because it is behaviour. That is the
506
+ // whole distinction, and it is what makes W-2 closable in this round and W-4's
507
+ // invariant half not.
508
+ //
509
+ // AND THE NEW PROSE CENSUS DOES NOT COVER THIS, which is the question the gap brief asks
510
+ // by name. `__tests__/withdrawn-claim-census.test.ts` gates ONE withdrawn security claim
511
+ // about the token channel, keyed on a channel cue bound to an exclusivity cue; W-4 is a
512
+ // different species — a comment that CONFLATES two conditions rather than one that
513
+ // over-claims a control — and widening that census to "any comment that conflates two
514
+ // conditions" is not mechanizable, and a vocabulary that tried would red on honest prose,
515
+ // which this round's own prohibition forbids.
516
+ //
517
+ // `connectDomains` NAMES THE SAME ONE ORIGIN, AND IT IS NOT OPTIONAL (408 plan 06 Task 2,
518
+ // MEASURED 2026-08-31). ext-apps' shipped schema documents `connectDomains` as mapping to
519
+ // `connect-src` with "empty or omitted -> no network connections (secure default)", and
520
+ // 408-02 D-H gave this wrapper a no-cors probe of the frame's origin — because Chromium
521
+ // fires `load` for a REFUSED connection, so `load` alone would label an error page `live`.
522
+ // With only `frameDomains` declared, a conformant host serves the App under
523
+ // `connect-src 'none'`, the probe is blocked before it leaves the document, and the
524
+ // wrapper settles in `unreachable-origin` for a live view that is perfectly reachable —
525
+ // measured in `app-host-harness.test.ts`, with Chromium naming the directive in its own
526
+ // console. That is not a blank frame, but it is a lie with a sentence attached, which is
527
+ // worse: it tells a person to go somewhere else while the pixels are right there.
528
+ //
529
+ // It is exactly as narrow as the frame allowance — the same single minted origin, never a
530
+ // wildcard and never a second configured value — so the App can reach the one host it
531
+ // embeds and nothing else.
532
+ ...frameDomain === void 0 ? {} : {
533
+ _meta: {
534
+ ui: { csp: { frameDomains: [frameDomain], connectDomains: [frameDomain] } }
535
+ }
536
+ }
537
+ }
538
+ ]
539
+ };
179
540
  };
180
541
  }
181
- function categorizeMcpTool(name) {
182
- if (name === "agent_qa_discover") return "discovery";
183
- if (name.includes("schema") || name.includes("definition") || name.includes("config")) return "schema";
184
- if (name.includes("_id")) return "id";
185
- if (name.includes("_hook")) return "hook";
186
- if (name.includes("classify")) return "triage";
187
- if (name.includes("_run") || name.includes("_logs") || name.includes("_artifact") || name.includes("_fix") || name.includes("_failure")) return "run";
188
- if (name.includes("_test") || name.includes("_suite")) return "authoring";
189
- return "unknown";
542
+ function originOf(liveUrl) {
543
+ try {
544
+ return new URL(liveUrl).origin;
545
+ } catch {
546
+ return void 0;
547
+ }
190
548
  }
191
- function isErrorResult(value) {
192
- return Boolean(value && typeof value === "object" && value.isError === true);
549
+ function registerAppResources(server, deps) {
550
+ const declared = deps.registry.list(deps.caller);
551
+ const mint = declared.find((candidate) => candidate.id === RUNS_LIVE_URL_FUNCTION_ID);
552
+ const roster = declared.find((candidate) => candidate.id === RUNS_EVENTS_FUNCTION_ID);
553
+ if (mint === void 0 || roster === void 0) return [];
554
+ if (mint.bindingStatus !== "bound") return [];
555
+ const read = createAppResourceReadHandler({
556
+ // The wall, bound once: every mint crosses `invokeFunction` with this host's registry and this
557
+ // caller, so input validation and the binding check run exactly as they do for a tool call.
558
+ // There is no store, no cache and no second data path.
559
+ invoke: (opId, input) => invokeFunction(deps.registry, opId, input, deps.caller),
560
+ maskText: deps.maskText,
561
+ // Absent stays ABSENT rather than becoming a present `undefined` supplier: the house rule for
562
+ // an optional member, and the two are the same posture to the read either way.
563
+ ...deps.liveLink === void 0 ? {} : { liveLink: deps.liveLink }
564
+ });
565
+ server.registerResource(
566
+ APP_RESOURCE_NAME,
567
+ // `list: undefined` is REQUIRED to be spelled out by the SDK, and here the explicit absence is
568
+ // load-bearing rather than merely honest: an unlisted template cannot be reached by
569
+ // enumeration, so no model walking `resources/list` can discover an address whose read mints a
570
+ // credential (Pitfall 2). Enumerating every watchable run is a listing this surface does not
571
+ // offer, and this is the line that says so.
572
+ new ResourceTemplate(APP_RESOURCE_URI_TEMPLATE, { list: void 0 }),
573
+ {
574
+ title: APP_RESOURCE_TITLE,
575
+ description: APP_RESOURCE_DESCRIPTION,
576
+ mimeType: APP_RESOURCE_MIME_TYPE
577
+ },
578
+ read
579
+ );
580
+ return [APP_RESOURCE_URI_TEMPLATE];
193
581
  }
194
- function isDashboardErrorResult(value) {
195
- const parsed = parseToolJsonContent(value);
196
- return Boolean(parsed && typeof parsed === "object" && parsed.ok === false);
582
+
583
+ // src/instructions.ts
584
+ var AGENT_QA_MCP_INSTRUCTIONS = [
585
+ "This project is tested with agent-qa. Read its runs and knowledge only through the agent_qa_* tools on this server, or the agent-qa CLI with --json.",
586
+ "Never open .agent-qa/, SQLite files, agent-qa-knowledge/memory/index/, memory/queue/, .lease or .epoch, and never list, glob or search those folders: they are agent-qa's runtime state.",
587
+ "Start with agent_qa_list_runs, then agent_qa_get_run_overview for one run.",
588
+ "The knowledge guide is agent-qa-knowledge/AGENTS.md; `npx agent-qa skills` lists the skills and their folder."
589
+ ].join("\n");
590
+
591
+ // src/mapping.ts
592
+ import {
593
+ AuthoringDraftEditInputSchema,
594
+ AuthoringDraftEditOutputSchema,
595
+ AuthoringDraftWriteInputSchema,
596
+ AuthoringDraftWriteOutputSchema,
597
+ AuthoringRefineFoldInputSchema,
598
+ AuthoringRefineFoldOutputSchema,
599
+ AuthoringVerifyDraftInputSchema,
600
+ AuthoringVerifyDraftOutputSchema,
601
+ BriefWhyInputSchema,
602
+ BriefWhyResultSchema,
603
+ ConfigAuthSetInputSchema,
604
+ ConfigChangeFoldInputSchema,
605
+ ConfigChangeFoldOutputSchema,
606
+ ConfigChangeOutputSchema,
607
+ ConfigFieldSetInputSchema,
608
+ ConfigHookAddInputSchema,
609
+ ConfigProjectScaffoldInputSchema,
610
+ ConfigTargetCreateInputSchema,
611
+ ConfigTargetEditInputSchema,
612
+ EntitiesGetInputSchema,
613
+ EntitiesResolveInputSchema,
614
+ EntityCardResolutionSchema,
615
+ EntityCardSchema,
616
+ KnowledgeAcceptChangeInputSchema,
617
+ KnowledgeBundlesListInputSchema,
618
+ KnowledgeBundlesListOutputSchema,
619
+ KnowledgeChangeFoldInputSchema,
620
+ KnowledgeChangeFoldOutputSchema,
621
+ KnowledgeForgetChangeInputSchema,
622
+ KnowledgeGatesListInputSchema,
623
+ KnowledgeGatesListResultSchema,
624
+ KnowledgeChangeOutputSchema,
625
+ KnowledgeRecordShowInputSchema,
626
+ KnowledgeRecordShowResultSchema,
627
+ KnowledgeRecordsDiffInputSchema,
628
+ KnowledgeRecordsDiffOutputSchema,
629
+ KnowledgeRecordsInspectInputSchema,
630
+ KnowledgeRecordsInspectOutputSchema,
631
+ KnowledgeRejectChangeInputSchema,
632
+ RunDigestSchema,
633
+ RunKnowledgeProjectionSchema,
634
+ RunOverviewProjectionSchema,
635
+ RunStartAcknowledgementSchema,
636
+ RunsArtifactInputSchema,
637
+ RunsArtifactOutputContract,
638
+ RunsDigestInputSchema,
639
+ RunsEvidenceFoldInputSchema,
640
+ RunsEvidenceFoldOutputContract,
641
+ RunsEvidenceInputSchema,
642
+ RunsEvidenceOutputContract,
643
+ RunsKnowledgeInputSchema,
644
+ RunsListInputSchema,
645
+ RunsListOutputContract,
646
+ RunsMarkInputSchema,
647
+ RunsMarkOutcomeSchema,
648
+ RunsMarkPreviewResultSchema,
649
+ RunsMarksHistoryInputSchema,
650
+ RunsMarksHistoryResultSchema,
651
+ RunsOverviewInputSchema,
652
+ RunsStartInputSchema,
653
+ TOOL_FOLD_DISCRIMINANT
654
+ } from "@vostride/agent-qa-schema";
655
+ var MAX_RESULT_SIZE_META_KEY = "anthropic/maxResultSizeChars";
656
+ var RUN_READ_MAX_RESULT_SIZE_CHARS = 1e5;
657
+ function compositionOf(projection) {
658
+ if (projection.ops.length === 1) return "single";
659
+ return projection.discriminant === void 0 ? "pipeline" : "union";
197
660
  }
198
- function parseToolJsonContent(value) {
199
- if (!value || typeof value !== "object") return void 0;
200
- const content = value.content;
201
- if (!Array.isArray(content)) return void 0;
202
- const firstText = content.find(
203
- (item) => item && typeof item === "object" && item.type === "text" && typeof item.text === "string"
204
- );
205
- if (!firstText) return void 0;
661
+ var AGENT_QA_OP_CONTRACTS = Object.freeze({
662
+ "runs.mark": Object.freeze({ input: RunsMarkInputSchema, output: RunsMarkOutcomeSchema }),
663
+ // 2026-09-15, 423-21 D-18: the feedback reads keep their ops' own strict contracts. The preview
664
+ // takes runs.mark's exact input and answers the read-only plan result.
665
+ "runs.marks.history": Object.freeze({
666
+ input: RunsMarksHistoryInputSchema,
667
+ output: RunsMarksHistoryResultSchema
668
+ }),
669
+ "runs.mark.preview": Object.freeze({ input: RunsMarkInputSchema, output: RunsMarkPreviewResultSchema }),
670
+ "knowledge.gates.list": Object.freeze({
671
+ input: KnowledgeGatesListInputSchema,
672
+ output: KnowledgeGatesListResultSchema
673
+ }),
674
+ "authoring.draft.write": Object.freeze({
675
+ input: AuthoringDraftWriteInputSchema,
676
+ output: AuthoringDraftWriteOutputSchema
677
+ }),
678
+ "authoring.draft.edit": Object.freeze({
679
+ input: AuthoringDraftEditInputSchema,
680
+ output: AuthoringDraftEditOutputSchema
681
+ }),
682
+ "authoring.draft.verify": Object.freeze({
683
+ input: AuthoringVerifyDraftInputSchema,
684
+ output: AuthoringVerifyDraftOutputSchema
685
+ }),
686
+ "runs.start": Object.freeze({
687
+ input: RunsStartInputSchema,
688
+ output: RunStartAcknowledgementSchema
689
+ }),
690
+ "config.field.set": Object.freeze({
691
+ input: ConfigFieldSetInputSchema,
692
+ output: ConfigChangeOutputSchema
693
+ }),
694
+ "config.target.create": Object.freeze({
695
+ input: ConfigTargetCreateInputSchema,
696
+ output: ConfigChangeOutputSchema
697
+ }),
698
+ "config.target.edit": Object.freeze({
699
+ input: ConfigTargetEditInputSchema,
700
+ output: ConfigChangeOutputSchema
701
+ }),
702
+ "config.auth.set": Object.freeze({
703
+ input: ConfigAuthSetInputSchema,
704
+ output: ConfigChangeOutputSchema
705
+ }),
706
+ "config.hook.add": Object.freeze({
707
+ input: ConfigHookAddInputSchema,
708
+ output: ConfigChangeOutputSchema
709
+ }),
710
+ "config.project.scaffold": Object.freeze({
711
+ input: ConfigProjectScaffoldInputSchema,
712
+ output: ConfigChangeOutputSchema
713
+ }),
714
+ // 2026-09-30 (432-12 D-05): the three knowledge changes answer what they applied.
715
+ "knowledge.accept": Object.freeze({
716
+ input: KnowledgeAcceptChangeInputSchema,
717
+ output: KnowledgeChangeOutputSchema
718
+ }),
719
+ "knowledge.reject": Object.freeze({
720
+ input: KnowledgeRejectChangeInputSchema,
721
+ output: KnowledgeChangeOutputSchema
722
+ }),
723
+ "knowledge.forget": Object.freeze({
724
+ input: KnowledgeForgetChangeInputSchema,
725
+ output: KnowledgeChangeOutputSchema
726
+ }),
727
+ "entities.resolve": Object.freeze({
728
+ input: EntitiesResolveInputSchema,
729
+ output: EntityCardResolutionSchema
730
+ }),
731
+ "entities.get": Object.freeze({
732
+ input: EntitiesGetInputSchema,
733
+ output: EntityCardSchema
734
+ }),
735
+ // 2026-09-28 (431-03, D-03/D-07): the canonical list contract — `RunsListOutputSchema` as the
736
+ // validator, the same object `runs.list` enforces with, plus its explicit output document.
737
+ "runs.list": Object.freeze({
738
+ input: RunsListInputSchema,
739
+ output: RunsListOutputContract
740
+ }),
741
+ "runs.digest": Object.freeze({
742
+ input: RunsDigestInputSchema,
743
+ output: RunDigestSchema
744
+ }),
745
+ // 2026-09-28 (431-04 D-04/D-05/D-07): the eight reads keep their ops' own contracts. The
746
+ // evidence arms use the canonical output contracts, whose validators are the runtime schemas.
747
+ "knowledge.bundles.list": Object.freeze({
748
+ input: KnowledgeBundlesListInputSchema,
749
+ output: KnowledgeBundlesListOutputSchema
750
+ }),
751
+ "knowledge.records.inspect": Object.freeze({
752
+ input: KnowledgeRecordsInspectInputSchema,
753
+ output: KnowledgeRecordsInspectOutputSchema
754
+ }),
755
+ "knowledge.records.diff": Object.freeze({
756
+ input: KnowledgeRecordsDiffInputSchema,
757
+ output: KnowledgeRecordsDiffOutputSchema
758
+ }),
759
+ "knowledge.record.show": Object.freeze({
760
+ input: KnowledgeRecordShowInputSchema,
761
+ output: KnowledgeRecordShowResultSchema
762
+ }),
763
+ "knowledge.brief.why": Object.freeze({
764
+ input: BriefWhyInputSchema,
765
+ output: BriefWhyResultSchema
766
+ }),
767
+ "runs.knowledge": Object.freeze({
768
+ input: RunsKnowledgeInputSchema,
769
+ output: RunKnowledgeProjectionSchema
770
+ }),
771
+ "runs.overview": Object.freeze({
772
+ input: RunsOverviewInputSchema,
773
+ output: RunOverviewProjectionSchema
774
+ }),
775
+ "runs.evidence": Object.freeze({
776
+ input: RunsEvidenceInputSchema,
777
+ output: RunsEvidenceOutputContract
778
+ }),
779
+ "runs.artifact": Object.freeze({
780
+ input: RunsArtifactInputSchema,
781
+ output: RunsArtifactOutputContract
782
+ })
783
+ });
784
+ var AGENT_QA_TOOL_PROJECTIONS = Object.freeze([
785
+ Object.freeze({
786
+ name: "agent_qa_write_test",
787
+ ops: Object.freeze(["authoring.draft.write"]),
788
+ kind: "workflow"
789
+ }),
790
+ Object.freeze({
791
+ name: "agent_qa_refine_test",
792
+ ops: Object.freeze(["authoring.draft.edit", "authoring.draft.verify"]),
793
+ kind: "workflow",
794
+ discriminant: TOOL_FOLD_DISCRIMINANT
795
+ }),
796
+ Object.freeze({
797
+ name: "agent_qa_start_run",
798
+ ops: Object.freeze(["runs.start"]),
799
+ kind: "workflow",
800
+ // 408 D-05: a run starts and is immediately watchable. The ack carries the runId, so the
801
+ // per-call reference is derived from the OUTPUT.
802
+ ui: Object.freeze({ resource: "live-run", runIdFrom: "output" })
803
+ }),
804
+ // 2026-09-30 (432-12 D-05): the config change tool writes in the call. The undo arm is gone.
805
+ Object.freeze({
806
+ name: "agent_qa_change_config",
807
+ ops: Object.freeze([
808
+ "config.field.set",
809
+ "config.target.create",
810
+ "config.target.edit",
811
+ "config.auth.set",
812
+ "config.hook.add",
813
+ "config.project.scaffold"
814
+ ]),
815
+ kind: "workflow",
816
+ discriminant: TOOL_FOLD_DISCRIMINANT
817
+ }),
818
+ // 2026-09-30 (432-12 D-05/D-09): accept, reject and forget apply in the call. Forget is the
819
+ // explicit removal; a wrong record is otherwise corrected by new feedback.
820
+ Object.freeze({
821
+ name: "agent_qa_change_knowledge",
822
+ ops: Object.freeze([
823
+ "knowledge.accept",
824
+ "knowledge.reject",
825
+ "knowledge.forget"
826
+ ]),
827
+ kind: "workflow",
828
+ discriminant: TOOL_FOLD_DISCRIMINANT
829
+ }),
830
+ Object.freeze({
831
+ name: "agent_qa_mark_run",
832
+ ops: Object.freeze(["runs.mark"]),
833
+ kind: "apply"
834
+ }),
835
+ // 2026-09-15, 423-21 D-18: marks history and the mark consequence preview beside the mark
836
+ // writer. Both are read projections with no apply envelope, so `agent_qa_mark_run` stays the
837
+ // only door that lands a mark.
838
+ Object.freeze({
839
+ name: "agent_qa_get_run_marks",
840
+ ops: Object.freeze(["runs.marks.history"]),
841
+ kind: "read"
842
+ }),
843
+ Object.freeze({
844
+ name: "agent_qa_preview_run_mark",
845
+ ops: Object.freeze(["runs.mark.preview"]),
846
+ kind: "read"
847
+ }),
848
+ Object.freeze({
849
+ name: "agent_qa_list_knowledge_gates",
850
+ ops: Object.freeze(["knowledge.gates.list"]),
851
+ kind: "read"
852
+ }),
853
+ // 2026-09-28, 431 D-04: the knowledge reads, named one-to-one with their CLI verbs.
854
+ Object.freeze({
855
+ name: "agent_qa_list_knowledge_bundles",
856
+ ops: Object.freeze(["knowledge.bundles.list"]),
857
+ kind: "read"
858
+ }),
859
+ Object.freeze({
860
+ name: "agent_qa_inspect_knowledge_records",
861
+ ops: Object.freeze(["knowledge.records.inspect"]),
862
+ kind: "read"
863
+ }),
864
+ Object.freeze({
865
+ name: "agent_qa_diff_knowledge_records",
866
+ ops: Object.freeze(["knowledge.records.diff"]),
867
+ kind: "read"
868
+ }),
869
+ Object.freeze({
870
+ name: "agent_qa_show_knowledge_record",
871
+ ops: Object.freeze(["knowledge.record.show"]),
872
+ kind: "read"
873
+ }),
874
+ Object.freeze({
875
+ name: "agent_qa_explain_brief",
876
+ ops: Object.freeze(["knowledge.brief.why"]),
877
+ kind: "read"
878
+ }),
879
+ Object.freeze({
880
+ name: "agent_qa_get_entity",
881
+ ops: Object.freeze(["entities.resolve", "entities.get"]),
882
+ kind: "read"
883
+ }),
884
+ Object.freeze({
885
+ name: "agent_qa_list_runs",
886
+ ops: Object.freeze(["runs.list"]),
887
+ kind: "read"
888
+ }),
889
+ Object.freeze({
890
+ name: "agent_qa_get_run_digest",
891
+ ops: Object.freeze(["runs.digest"]),
892
+ kind: "read",
893
+ // 408 D-05/D-07: the watch/status read, and therefore the LATE-ATTACH door — a run started
894
+ // from the CLI, from CI or from the dashboard is watchable through it exactly as an
895
+ // MCP-started one is, because a viewer attaches and never owns. Its runId is on the INPUT.
896
+ ui: Object.freeze({ resource: "live-run", runIdFrom: "input" })
897
+ }),
898
+ // 2026-09-28, 431 D-05/D-06/D-07: one completed run's whole reads. The overview is the map, run
899
+ // knowledge the whole used-and-learned projection unchanged, and evidence one addressed document
900
+ // or blob. All three carry the result-size hint.
901
+ Object.freeze({
902
+ name: "agent_qa_get_run_overview",
903
+ ops: Object.freeze(["runs.overview"]),
904
+ kind: "read",
905
+ maxResultSizeChars: RUN_READ_MAX_RESULT_SIZE_CHARS
906
+ }),
907
+ Object.freeze({
908
+ name: "agent_qa_get_run_knowledge",
909
+ ops: Object.freeze(["runs.knowledge"]),
910
+ kind: "read",
911
+ maxResultSizeChars: RUN_READ_MAX_RESULT_SIZE_CHARS
912
+ }),
913
+ Object.freeze({
914
+ name: "agent_qa_get_run_evidence",
915
+ ops: Object.freeze(["runs.evidence", "runs.artifact"]),
916
+ kind: "read",
917
+ discriminant: TOOL_FOLD_DISCRIMINANT,
918
+ maxResultSizeChars: RUN_READ_MAX_RESULT_SIZE_CHARS
919
+ })
920
+ ]);
921
+ var AGENT_QA_COMPOSED_CONTRACTS = Object.freeze({
922
+ agent_qa_refine_test: Object.freeze({
923
+ input: AuthoringRefineFoldInputSchema,
924
+ output: AuthoringRefineFoldOutputSchema
925
+ }),
926
+ agent_qa_change_config: Object.freeze({
927
+ input: ConfigChangeFoldInputSchema,
928
+ output: ConfigChangeFoldOutputSchema
929
+ }),
930
+ agent_qa_change_knowledge: Object.freeze({
931
+ input: KnowledgeChangeFoldInputSchema,
932
+ output: KnowledgeChangeFoldOutputSchema
933
+ }),
934
+ // 2026-09-28 (431-04 D-07): the evidence fold, composed in the schema package from both arms'
935
+ // canonical contracts and routed by full op id.
936
+ agent_qa_get_run_evidence: Object.freeze({
937
+ input: RunsEvidenceFoldInputSchema,
938
+ output: RunsEvidenceFoldOutputContract
939
+ }),
940
+ agent_qa_get_entity: Object.freeze({
941
+ input: EntitiesResolveInputSchema,
942
+ output: EntityCardResolutionSchema
943
+ })
944
+ });
945
+ function advertisedContracts(projection) {
946
+ const composed = AGENT_QA_COMPOSED_CONTRACTS[projection.name];
947
+ if (composed !== void 0) return composed;
948
+ const [only] = projection.ops;
949
+ return only === void 0 ? void 0 : AGENT_QA_OP_CONTRACTS[only];
950
+ }
951
+ var DEGRADED_OUTPUT_CONTRACTS = Object.freeze([
952
+ // 397 DISPOSITION (2026-08-16 · D-20): keep-degraded. `agent_qa_start_run` advertises no
953
+ // `outputSchema` and keeps it that way. The cause is a property of the CONTRACT, not of this
954
+ // surface — a branded run id is `z.string().transform(asRunId)`, and the converter refuses a
955
+ // transform because there is no true JSON-Schema for "and then this function runs". The two ways
956
+ // out are both worse: advertising the input-mode document is the second-recipe lie D-16 forbids,
957
+ // and un-branding the id to please a converter would spend a type-safety property of the whole
958
+ // codebase on one surface's listing. 397 rules the degradation ADJUDICATED and CLOSED rather than
959
+ // pending; re-opening it means changing the contract, which is a golden-diff-visible act. The
960
+ // absence itself is not re-pinned here — `projection.ts` probes the conversion both ways and the
961
+ // shipped projection suite owns that fact (RESEARCH Pitfall 9); the conformance suite asserts
962
+ // only the join.
963
+ Object.freeze({
964
+ tool: "agent_qa_start_run",
965
+ opId: "runs.start",
966
+ schema: "RunStartAcknowledgementSchema",
967
+ measured: "Transforms cannot be represented in JSON Schema",
968
+ adjudication: "397"
969
+ })
970
+ // 2026-09-28 (431-03, D-03/D-07 · research A1): the `agent_qa_list_runs` row is REMOVED, not
971
+ // re-ruled. Its objection stands — no hand-written array schema is advertised. What changed is the
972
+ // contract: `runs.list` now declares `RunsListOutputContract`, whose validator is the same
973
+ // `RunsListOutputSchema` object and whose output document is derived from the runtime schema's
974
+ // own output conversion. It converts cleanly, so the probe in `projection.ts` would refuse the row.
975
+ ]);
976
+ var UNPROJECTED_ADJUDICATIONS = Object.freeze(["397", "398", "405", "408", "428"]);
977
+ var UNPROJECTED_OPS = Object.freeze([
978
+ Object.freeze({
979
+ opId: "agent.ask",
980
+ reason: "D-03 (dated 2026-08-15): ask-over-MCP is nested-agent delegation \u2014 trust, token-driven LLM spend, lease interaction and streaming are four separate problems and no MCPS requirement asks for any of them. Deferred as its own seam, not omitted by oversight.",
981
+ adjudication: "397"
982
+ }),
983
+ // 2026-10-01 (432-17, D-05): `proposals.accept` and `proposals.reject` (397, requiresHumanOrigin)
984
+ // are deleted with the inbox and leave the scoping note.
985
+ // 2026-10-01 (432-18, D-14/D-15): `grants.revoke` (397, requiresHumanOrigin) and `grants.list`
986
+ // (397, not a mid-flight read) are deleted with the grant ledger and leave the scoping note.
987
+ Object.freeze({
988
+ opId: "runs.get",
989
+ reason: "not a mid-flight workflow read per D-02 \u2014 the entity door renders a run's card and the digest carries what triage needs; 397 adjudicates",
990
+ adjudication: "397"
991
+ }),
992
+ Object.freeze({
993
+ opId: "runs.events",
994
+ reason: "not a mid-flight workflow read per D-02, and a stream op besides: a `tools/call` is one round trip and has no shape for a resumable event feed; 397 adjudicates",
995
+ adjudication: "397"
996
+ }),
997
+ Object.freeze({
998
+ opId: "runs.postprocessing",
999
+ reason: "not a mid-flight workflow read per D-02; 397 adjudicates",
1000
+ adjudication: "397"
1001
+ }),
1002
+ Object.freeze({
1003
+ opId: "runs.detail",
1004
+ reason: "D-03 (dated 2026-08-27, 405-01): not a mid-flight workflow read per D-02 \u2014 this is a COMPLETED run's projected step tree, read by the durable run-detail page, and the entity door already renders a run's card while the digest carries what triage needs. The whole projection of a run is also a large one-shot payload with no shape a `tools/call` round trip wants. Deferred as its own seam, not omitted.",
1005
+ adjudication: "405"
1006
+ }),
1007
+ Object.freeze({
1008
+ opId: "runs.liveurl",
1009
+ reason: "D-03 (dated 2026-08-25, 403-01): the op MINTS A CREDENTIAL \u2014 a short-lived, view-scoped capability token for a running browser. Whether an agent-origin MCP caller may mint one, and what it may hand it to, is Phase 408's question (the MCP App embed); projecting it here would answer that question by accident. Deferred as its own seam, not omitted. ANSWERED (dated 2026-08-31, 408 D-01): the deferral above is discharged, and the ruling is INTERNAL MINT ONLY. The MCP server invokes this op BY REFERENCE, inside its App-rendering path and nowhere else, and the minted token lands only in the host-rendered HTML of the unlisted `ui://agent-qa/live-run/{runId}` App resource \u2014 never in tool-result text a model reads. Projecting a mint tool stays REJECTED: a live credential in model context is the T-395-R5-10 shape, and masking it would leave a tool that hands back nothing a host could use. The op therefore STAYS unprojected BY RULING rather than by deferral \u2014 answered on the record, never silently reclassified. RE-GROUNDED (dated 2026-09-01, 408 gap round, road `demote`; the full ruling is in `app-resource.ts`'s header). The answer above STANDS and one half of its stated property is corrected rather than deleted. Whether the document this server serves is host-rendered HTML or text a model consumes is a property of the CALLER, not of this server, so the \"never in tool-result text a model reads\" half over-claimed. What this server guarantees is narrower and it is verifiable: it writes the token into exactly one place, the frame src of the document the `ui://` read serves, and that door is reachable by any caller of a composition whose mint is bound (dated 2026-10-01, 432-20/432-21, D-12: the scope gate that once also bounded it is deleted for every caller, and the loopback bind and the Host/Origin guards still bound who can reach it). The op STAYS unprojected on exactly the same footing, and the reason is unchanged and STRONGER rather than weaker: a projected mint tool would put the credential on a channel a model consumes BY DEFAULT, on every call, for every caller, whereas the `ui://` read is an unlisted address a caller must already know and deliberately ask for.",
1010
+ adjudication: "408"
1011
+ }),
1012
+ Object.freeze({
1013
+ opId: "editor.liveurl",
1014
+ reason: "D-07 (dated 2026-09-25, 428): the op MINTS A CREDENTIAL \u2014 a view credential for a live EDITOR session, which is private to the dashboard page that owns it; it is reachable only through the dashboard door and never through an MCP tool or the MCP App live-run resource. Projecting it would give an agent a watch carrier for a draft execution, which the owner declined in D-07. Unprojected by ruling, not by deferral.",
1015
+ adjudication: "428"
1016
+ }),
1017
+ Object.freeze({
1018
+ opId: "config.get",
1019
+ reason: "not a mid-flight workflow read per D-02; 397 adjudicates",
1020
+ adjudication: "397"
1021
+ }),
1022
+ Object.freeze({
1023
+ opId: "analysis.get",
1024
+ reason: "not a mid-flight workflow read per D-02; 397 adjudicates",
1025
+ adjudication: "397"
1026
+ }),
1027
+ Object.freeze({
1028
+ opId: "authoring.funnel.get",
1029
+ reason: "not a mid-flight workflow read per D-02; 397 adjudicates",
1030
+ adjudication: "397"
1031
+ }),
1032
+ Object.freeze({
1033
+ opId: "quarantine.add",
1034
+ reason: "not a mid-flight workflow write per D-02; 397 adjudicates",
1035
+ adjudication: "397"
1036
+ }),
1037
+ Object.freeze({
1038
+ opId: "quarantine.release",
1039
+ reason: "not a mid-flight workflow write per D-02; 397 adjudicates",
1040
+ adjudication: "397"
1041
+ }),
1042
+ Object.freeze({
1043
+ opId: "quarantine.list",
1044
+ reason: "not a mid-flight workflow read per D-02; 397 adjudicates",
1045
+ adjudication: "397"
1046
+ }),
1047
+ // 398 D-09 (dated 2026-08-17): the twentieth entry, and the FIRST op ever dispositioned here that
1048
+ // 397 did not rule on — because it did not exist then. Its exclusion is argued rather than scoped,
1049
+ // which is why it does not take the mid-flight line: `agent.explain` answers "what can you do",
1050
+ // and 397 already ruled the v1 discovery tool NOT a gap on this surface, because the protocol
1051
+ // answers machine discovery itself. Projecting it anyway would be the third answer that ruling
1052
+ // refuses.
1053
+ Object.freeze({
1054
+ opId: "agent.explain",
1055
+ reason: "D-09 (dated 2026-08-17): the protocol answers machine discovery itself \u2014 `tools/list` plus SEP-2575 `server/discover` \u2014 and 398's `init --agent` manifest answers agent discovery from the committed catalog. This op answers a PERSON asking in prose, which is a third question with a third audience. 397 ruled the v1 discovery TOOL not-a-gap on exactly this reasoning; projecting the op would reinstate it under a new name.",
1056
+ adjudication: "398"
1057
+ })
1058
+ // 2026-09-30 (432-12 D-05): the five pre-432 proposal ops went off the catalogue with their
1059
+ // tools. 2026-10-01 (432-17 D-05): they are deleted from the registry and leave the scoping note.
1060
+ ]);
1061
+ var PROJECTED_RULING_ADDENDA = Object.freeze([
1062
+ Object.freeze({
1063
+ opId: "knowledge.bundles.list",
1064
+ ruledBy: "397",
1065
+ ruling: "not a mid-flight workflow read per D-02; 397 adjudicates",
1066
+ addendum: "D-01/D-04 (dated 2026-09-28, 431-04): projected as `agent_qa_list_knowledge_bundles`. A coding agent in a fresh workspace reads knowledge only through structured channels, so the committed-knowledge reads are served here rather than left to files; this supersedes the 2026-09-14 decision that projected the six safe reads only, whose no-search half stands (D-09)."
1067
+ }),
1068
+ Object.freeze({
1069
+ opId: "knowledge.records.inspect",
1070
+ ruledBy: "397",
1071
+ ruling: "not a mid-flight workflow read per D-02; 397 adjudicates",
1072
+ addendum: "D-01/D-04 (dated 2026-09-28, 431-04): projected as `agent_qa_inspect_knowledge_records`. A coding agent in a fresh workspace reads knowledge only through structured channels, so the committed-knowledge reads are served here rather than left to files; this supersedes the 2026-09-14 decision that projected the six safe reads only, whose no-search half stands (D-09)."
1073
+ }),
1074
+ Object.freeze({
1075
+ opId: "knowledge.records.diff",
1076
+ ruledBy: "397",
1077
+ ruling: "not a mid-flight workflow read per D-02; 397 adjudicates",
1078
+ addendum: "D-01/D-04 (dated 2026-09-28, 431-04): projected as `agent_qa_diff_knowledge_records`. A coding agent in a fresh workspace reads knowledge only through structured channels, so the committed-knowledge reads are served here rather than left to files; this supersedes the 2026-09-14 decision that projected the six safe reads only, whose no-search half stands (D-09)."
1079
+ }),
1080
+ Object.freeze({
1081
+ opId: "knowledge.record.show",
1082
+ ruledBy: "397",
1083
+ ruling: "not a mid-flight workflow read per D-02; 397 adjudicates",
1084
+ addendum: "D-01/D-04 (dated 2026-09-28, 431-04): projected as `agent_qa_show_knowledge_record`. A coding agent in a fresh workspace reads knowledge only through structured channels, so the committed-knowledge reads are served here rather than left to files; this supersedes the 2026-09-14 decision that projected the six safe reads only, whose no-search half stands (D-09)."
1085
+ }),
1086
+ Object.freeze({
1087
+ opId: "knowledge.brief.why",
1088
+ ruledBy: "397",
1089
+ ruling: "not a mid-flight workflow read per D-02; 397 adjudicates",
1090
+ addendum: "D-01/D-04 (dated 2026-09-28, 431-04): projected as `agent_qa_explain_brief`. A coding agent in a fresh workspace reads knowledge only through structured channels, so the committed-knowledge reads are served here rather than left to files; this supersedes the 2026-09-14 decision that projected the six safe reads only, whose no-search half stands (D-09)."
1091
+ }),
1092
+ Object.freeze({
1093
+ opId: "runs.knowledge",
1094
+ ruledBy: "407",
1095
+ ruling: "D-05 (dated 2026-08-29, 407-01): not a mid-flight workflow read per D-02 \u2014 this is a COMPLETED run's used-and-learned projection, read by the durable Knowledge tab, and it is unprojected on exactly the footing its sibling `runs.detail` is. It carries every step's three served brief channels, each one's serve-time retrieval trace and the truncated candidate ledger beside it, so it is a large one-shot payload with no shape a `tools/call` round trip wants \u2014 larger than the step tree, not smaller. A planner mid-flight also has no use for what a FINISHED run was briefed with; the entity door already renders a record's card and the digest carries what triage needs. Deferred as its own seam, not omitted.",
1096
+ addendum: "D-05 (dated 2026-09-28, 431-04): projected as `agent_qa_get_run_knowledge`, answering the whole `runs.knowledge` projection unchanged; the owner declined a step filter. The size objection above is answered in these terms: `agent_qa_get_run_overview` is the entry point that maps a run without bodies, `agent_qa_explain_brief` and `agent_qa_show_knowledge_record` are the drill-down, and the tool carries a 100000-character result-size hint so a host keeps the one-shot answer whole."
1097
+ }),
1098
+ Object.freeze({
1099
+ opId: "runs.evidence",
1100
+ ruledBy: "423",
1101
+ ruling: "D-04 (dated 2026-09-15, 423-21): not a mid-flight workflow read per D-02 \u2014 this is one COMPLETED run step's parsed evidence document (console, network, variables, accessibility or ARIA), read by the dashboard evidence tabs, and it is unprojected on exactly the footing its sibling `runs.detail` is. A network document alone may carry hundreds of records, so it is a large one-shot payload with no shape a `tools/call` round trip wants, and a planner mid-flight has no use for a finished step's captured traffic. The raw SHA bytes stay on `runs.artifact` and the two HTTP doors. Deferred as its own seam, not omitted.",
1102
+ addendum: "D-07 (dated 2026-09-28, 431-04): projected as the `runs.evidence` arm of `agent_qa_get_run_evidence`. A caller asks for ONE step document by the exact address `agent_qa_get_run_overview` supplies, so the answer is one addressed document rather than a run's captured traffic. Masking is unchanged: the op stays on the cold read-scope allowlist. The tool carries the 100000-character result-size hint."
1103
+ }),
1104
+ Object.freeze({
1105
+ opId: "runs.artifact",
1106
+ ruledBy: "405",
1107
+ ruling: "D-12/D-13 (dated 2026-08-27, 405-05): not a mid-flight workflow read per D-02 \u2014 this resolves ONE completed run's content-addressed evidence blob, and its result is a base64 body. A `tools/call` round trip is the wrong carriage for bytes: a conversation cannot render a PNG, and a tool that returned one would spend a context window on a payload no reader can read. The two HTTP doors are its carriage, and 408's embed is the surface that renders it. Deferred as its own seam, not omitted. Phase 424 (dated 2026-09-20): the recording fact rides runs.detail; runs.artifact answers a bounded slice when range is supplied; pulling a whole recording through unary base64 is permitted and not recommended; no recording tool is projected because the MX ceiling of 22 is fully allocated.",
1108
+ addendum: "D-07 (dated 2026-09-28, 431-04): projected as the `runs.artifact` arm of `agent_qa_get_run_evidence`. The blob is addressed by the sha the overview supplies, with no step-to-sha lookup inside the tool, and the rendering objection is answered by the image result arm 431-05 adds. The Phase 424 note that the MX ceiling of 22 was fully allocated is superseded: the ceiling is 24 (D-01)."
1109
+ })
1110
+ ]);
1111
+
1112
+ // src/masked-result.ts
1113
+ import { RunsArtifactResultSchema } from "@vostride/agent-qa-schema";
1114
+ var WITHHELD_TEXT = "This result could not be returned: part of its output could not be masked in place \u2014 a value whose hidden form would not be the same kind of value, or two names that mask to one name \u2014 so the unmasked object was withheld rather than served.";
1115
+ var UnmaskableValueError = class extends Error {
1116
+ };
1117
+ function maskJsonLeaves(value, maskText) {
1118
+ if (typeof value === "string") return maskText(value);
1119
+ if (value === null) return null;
1120
+ if (Array.isArray(value)) {
1121
+ return value.map((entry) => maskJsonLeaves(entry, maskText));
1122
+ }
1123
+ if (typeof value === "object") {
1124
+ const masked = {};
1125
+ for (const [key, entry] of Object.entries(value)) {
1126
+ const maskedKey = maskText(key);
1127
+ if (Object.hasOwn(masked, maskedKey)) {
1128
+ throw new UnmaskableValueError("two keys of one object mask to the same name");
1129
+ }
1130
+ Object.defineProperty(masked, maskedKey, {
1131
+ value: maskJsonLeaves(entry, maskText),
1132
+ enumerable: true,
1133
+ writable: true,
1134
+ configurable: true
1135
+ });
1136
+ }
1137
+ return masked;
1138
+ }
1139
+ const spelling = JSON.stringify(value) ?? "null";
1140
+ if (maskText(spelling) !== spelling) {
1141
+ throw new UnmaskableValueError("a non-string leaf spells a registered value");
1142
+ }
1143
+ return value;
1144
+ }
1145
+ function maskJsonDocument(output, maskText) {
1146
+ const serialized = JSON.stringify(output) ?? "null";
1147
+ let parsed;
1148
+ try {
1149
+ parsed = JSON.parse(serialized);
1150
+ } catch {
1151
+ return void 0;
1152
+ }
1153
+ let value;
206
1154
  try {
207
- return JSON.parse(firstText.text);
1155
+ value = maskJsonLeaves(parsed, maskText);
208
1156
  } catch {
209
1157
  return void 0;
210
1158
  }
1159
+ const text = JSON.stringify(value);
1160
+ if (text === void 0) return void 0;
1161
+ return { value, text };
211
1162
  }
212
- function categorizeMcpError(errOrResult) {
213
- if (errOrResult instanceof z.ZodError) return "validation";
214
- const parsed = parseToolJsonContent(errOrResult);
215
- const candidate = parsed && typeof parsed === "object" ? parsed : void 0;
216
- if (candidate && candidate.ok === false) return "dashboard_error";
217
- if (errOrResult instanceof Error) return "tool_error";
218
- if (isErrorResult(errOrResult)) return "tool_error";
219
- return "unknown";
1163
+ function maskedToolResult(output, maskText) {
1164
+ const masked = maskJsonDocument(output, maskText);
1165
+ if (masked === void 0) {
1166
+ return {
1167
+ content: [{ type: "text", text: WITHHELD_TEXT }],
1168
+ isError: true
1169
+ };
1170
+ }
1171
+ return {
1172
+ content: [{ type: "text", text: masked.text }],
1173
+ structuredContent: masked.value
1174
+ };
220
1175
  }
221
- function extractExplicitCanonicalRunId(input) {
222
- if (!input || typeof input !== "object" || Array.isArray(input)) return void 0;
223
- const runId = input.runId;
224
- return typeof runId === "string" && isCanonicalId("run", runId) ? runId : void 0;
1176
+ function maskedToolError(err, maskText) {
1177
+ const message = err instanceof Error ? err.message : String(err);
1178
+ return {
1179
+ content: [
1180
+ {
1181
+ type: "text",
1182
+ text: maskText(message)
1183
+ }
1184
+ ],
1185
+ isError: true
1186
+ };
225
1187
  }
226
- function extractSuccessfulEnqueueRunId(result) {
227
- if (isErrorResult(result)) return void 0;
228
- const parsed = parseToolJsonContent(result);
229
- const container = parsed && typeof parsed === "object" ? parsed : void 0;
230
- if (!container) return void 0;
231
- const body = container.body && typeof container.body === "object" ? container.body : container;
232
- for (const key of ["runId", "id", "parentRunId"]) {
233
- const value = body[key];
234
- if (typeof value === "string" && isCanonicalId("run", value)) return value;
1188
+ var RUN_EVIDENCE_IMAGE_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg"]);
1189
+ var RUN_EVIDENCE_IMAGE_TEXT_MARKER = "[image attached as image content]";
1190
+ var PNG_SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10];
1191
+ var PNG_IEND_TRAILER = [
1192
+ 0,
1193
+ 0,
1194
+ 0,
1195
+ 0,
1196
+ 73,
1197
+ 69,
1198
+ 78,
1199
+ 68,
1200
+ 174,
1201
+ 66,
1202
+ 96,
1203
+ 130
1204
+ ];
1205
+ var JPEG_START = [255, 216, 255];
1206
+ var JPEG_END = [255, 217];
1207
+ var CANONICAL_BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
1208
+ function refusalText(refusal, contentType) {
1209
+ switch (refusal) {
1210
+ case "artifact-range-fragment":
1211
+ return "artifact-range-fragment: agent_qa_get_run_evidence returns a screenshot only as one complete PNG or JPEG image, and this range covers part of one. Call runs.artifact without a range.";
1212
+ case "artifact-not-image":
1213
+ return `artifact-not-image: this artifact is ${contentType ?? "not typed"}, not a PNG or JPEG screenshot, and agent_qa_get_run_evidence relays no other artifact bytes. Read a step document through action runs.evidence.`;
1214
+ case "artifact-image-invalid":
1215
+ return "artifact-image-invalid: the artifact is typed as an image, but its bytes are not one complete PNG or JPEG, so they were not returned.";
1216
+ case "artifact-image-withheld":
1217
+ return "artifact-image-withheld: masking changed the encoded screenshot, so neither the image nor its bytes were returned.";
235
1218
  }
236
- return void 0;
237
1219
  }
238
- function resolveDashboardApiUrl(dashboardUrl, apiPath) {
239
- const base = dashboardUrl.endsWith("/") ? dashboardUrl : `${dashboardUrl}/`;
240
- const normalizedPath = apiPath.startsWith("/") ? apiPath.slice(1) : apiPath;
241
- return new URL(normalizedPath, base).toString();
1220
+ function hasBytesAt(bytes, expected, offset) {
1221
+ if (offset < 0 || bytes.length < offset + expected.length) return false;
1222
+ return expected.every((byte, index) => bytes[offset + index] === byte);
1223
+ }
1224
+ function isCompleteImage(contentType, bytes) {
1225
+ if (contentType === "image/png") {
1226
+ return hasBytesAt(bytes, PNG_SIGNATURE, 0) && hasBytesAt(bytes, PNG_IEND_TRAILER, bytes.length - PNG_IEND_TRAILER.length);
1227
+ }
1228
+ return hasBytesAt(bytes, JPEG_START, 0) && hasBytesAt(bytes, JPEG_END, bytes.length - JPEG_END.length);
1229
+ }
1230
+ function decodeCanonicalBase64(text) {
1231
+ if (!CANONICAL_BASE64.test(text)) return void 0;
1232
+ let binary;
1233
+ try {
1234
+ binary = atob(text);
1235
+ } catch {
1236
+ return void 0;
1237
+ }
1238
+ if (btoa(binary) !== text) return void 0;
1239
+ return Uint8Array.from(binary, (char) => char.charCodeAt(0));
242
1240
  }
243
- function withQuery(path, query) {
244
- const params = new URLSearchParams();
245
- for (const [key, value] of Object.entries(query)) {
246
- if (value !== void 0) params.set(key, String(value));
1241
+ function coversWholeBlob(artifact) {
1242
+ if (artifact.range === void 0) {
1243
+ return artifact.totalLength === void 0 || artifact.totalLength === artifact.byteLength;
247
1244
  }
248
- const queryString = params.toString();
249
- return queryString ? `${path}?${queryString}` : path;
1245
+ return artifact.totalLength !== void 0 && artifact.range.start === 0 && artifact.range.end === artifact.totalLength - 1 && artifact.byteLength === artifact.totalLength;
250
1246
  }
251
- async function callDashboardApi(input) {
252
- const response = await fetch(resolveDashboardApiUrl(input.dashboardUrl, input.path), {
253
- method: input.method ?? "GET",
254
- headers: input.body === void 0 ? void 0 : { "content-type": "application/json" },
255
- body: input.body === void 0 ? void 0 : JSON.stringify(input.body)
1247
+ function decorateRunEvidenceImage(input) {
1248
+ const { result } = input;
1249
+ if (result.isError === true) return result;
1250
+ const refuse = (refusal, contentType) => ({
1251
+ content: [{ type: "text", text: input.maskText(refusalText(refusal, contentType)) }],
1252
+ isError: true,
1253
+ ...result._meta === void 0 ? {} : { _meta: result._meta }
256
1254
  });
257
- const text = await response.text();
258
- let body = null;
259
- if (text.length > 0) {
260
- try {
261
- body = JSON.parse(text);
262
- } catch {
263
- body = text;
264
- }
1255
+ const parsed = RunsArtifactResultSchema.safeParse(input.output);
1256
+ if (!parsed.success) return refuse("artifact-image-invalid");
1257
+ const artifact = parsed.data;
1258
+ if (!RUN_EVIDENCE_IMAGE_TYPES.has(artifact.contentType)) {
1259
+ return refuse("artifact-not-image", artifact.contentType);
1260
+ }
1261
+ if (!coversWholeBlob(artifact)) return refuse("artifact-range-fragment");
1262
+ const masked = result.structuredContent;
1263
+ if (masked?.bytesBase64 !== artifact.bytesBase64 || masked.contentType !== artifact.contentType) {
1264
+ return refuse("artifact-image-withheld");
1265
+ }
1266
+ const bytes = decodeCanonicalBase64(artifact.bytesBase64);
1267
+ if (bytes === void 0 || bytes.length !== artifact.byteLength || !isCompleteImage(artifact.contentType, bytes)) {
1268
+ return refuse("artifact-image-invalid");
265
1269
  }
1270
+ const text = JSON.stringify({ ...masked, bytesBase64: RUN_EVIDENCE_IMAGE_TEXT_MARKER });
266
1271
  return {
267
- ok: response.ok,
268
- status: response.status,
269
- body
1272
+ ...result,
1273
+ content: [
1274
+ ...result.content.map((block) => block.type === "text" ? { type: "text", text } : block),
1275
+ { type: "image", data: artifact.bytesBase64, mimeType: artifact.contentType }
1276
+ ]
270
1277
  };
271
1278
  }
272
- function collectStrings(value, output = [], limit = 80) {
273
- if (output.length >= limit) return output;
274
- if (typeof value === "string" && value.trim().length > 0) {
275
- output.push(value);
276
- } else if (Array.isArray(value)) {
277
- for (const item of value) collectStrings(item, output, limit);
278
- } else if (value && typeof value === "object") {
279
- for (const child of Object.values(value)) collectStrings(child, output, limit);
280
- }
281
- return output;
1279
+
1280
+ // src/migration-map.ts
1281
+ var V1_TOOL_MIGRATION_MAP = Object.freeze([
1282
+ // 1
1283
+ Object.freeze({
1284
+ v1Name: "agent_qa_discover",
1285
+ arm: "retired",
1286
+ note: "not-a-gap (397, 2026-08-16): the protocol answers discovery itself, so a hand-written catalogue tool beside it would be a second, drifting answer to one question.",
1287
+ route: "`tools/list` (plus SEP-2575 `server/discover`) answers machine discovery; 398's build-time capability catalog, carried by `agent-qa init --agent`, answers the human-facing version."
1288
+ }),
1289
+ // 2
1290
+ Object.freeze({
1291
+ v1Name: "agent_qa_get_config",
1292
+ arm: "retired",
1293
+ note: "A4 adjudication (2026-08-17, phase 398 plan 02): the inventory rules this row `n/a -> projected by 396`, but `config.get` is in UNPROJECTED_OPS after 396 plan 04 \u2014 not a mid-flight workflow read per D-02 \u2014 so there is no live tool to rename to.",
1294
+ route: "no MCP tool reads configuration; the workspace posture an agent needs reaches it through `agent-qa init --agent`'s manifest (398 D-16), by env-ref name and never by value."
1295
+ }),
1296
+ // 3
1297
+ Object.freeze({
1298
+ v1Name: "agent_qa_schema_reference",
1299
+ arm: "retired",
1300
+ note: "not-a-gap (397, 2026-08-16): a served COPY of a contract is exactly the drift that advertising the live contract replaced.",
1301
+ route: "every tool advertises its op's live `inputSchema`/`outputSchema` in `tools/list` (396 D-16), so a caller reads the contract the gate actually validates with; a fold lists its arms' fields as one object, and its `action` field says which fields each action takes."
1302
+ }),
1303
+ // 4
1304
+ Object.freeze({
1305
+ v1Name: "agent_qa_validate_definition",
1306
+ arm: "retired",
1307
+ note: "not-a-gap (397, 2026-08-16): a standalone linter over arbitrary text is a second validator that can disagree with the one that gates writes.",
1308
+ route: "validation reaches a caller as `authoring.draft.verify`'s verdict inside the authoring funnel \u2014 the verify arm of `agent_qa_refine_test`."
1309
+ }),
1310
+ // 5
1311
+ Object.freeze({
1312
+ v1Name: "agent_qa_generate_id",
1313
+ arm: "retired",
1314
+ note: "not-a-gap (397, 2026-08-16): died with the CRUD model. Minting a canonical id on a surface is what 394-11 removed from the CLI, for this same reason.",
1315
+ route: "ids are minted host-side where the entity is written; a caller names what it wants and reads the id back off the entity card `agent_qa_get_entity` returns."
1316
+ }),
1317
+ // 6
1318
+ Object.freeze({
1319
+ v1Name: "agent_qa_validate_id",
1320
+ arm: "retired",
1321
+ note: "not-a-gap (397, 2026-08-16): died with the CRUD model. A standalone validator would be a second answer to a question already answered at every parse point.",
1322
+ route: "the one URI codec (`packages/schema/src/entity/uri.ts`) validates every `agentqa://` reference as it is parsed, including the one `agent_qa_get_entity` resolves."
1323
+ }),
1324
+ // 7
1325
+ Object.freeze({
1326
+ v1Name: "agent_qa_list_tests",
1327
+ arm: "pending",
1328
+ note: "gap (397, 2026-08-16): nothing in the registry enumerates the test corpus \u2014 `entities.resolve` resolves a reference to an entity, which is a different question.",
1329
+ route: "until the owning milestone mints `entities.list`, a known reference resolves through `agent_qa_get_entity`; enumeration has no tool.",
1330
+ owner: "M5 (live view & run invocation)"
1331
+ }),
1332
+ // 8
1333
+ Object.freeze({
1334
+ v1Name: "agent_qa_read_test",
1335
+ arm: "renamed-to",
1336
+ v2Tool: "agent_qa_get_entity",
1337
+ note: "projected by 396 (inventory row 8, 2026-08-16): `entities.get` is D-02's universal entity read, kind TST, behind the one read door."
1338
+ }),
1339
+ // 9
1340
+ Object.freeze({
1341
+ v1Name: "agent_qa_validate_test",
1342
+ arm: "retired",
1343
+ note: "not-a-gap (397, 2026-08-16): row 4's ruling. `authoring.draft.verify` is the only validator, and it validates a draft in the funnel rather than arbitrary text a caller pasted.",
1344
+ route: "the verify arm of `agent_qa_refine_test` returns the same verdict that gates the write."
1345
+ }),
1346
+ // 10
1347
+ Object.freeze({
1348
+ v1Name: "agent_qa_create_test",
1349
+ arm: "renamed-to",
1350
+ v2Tool: "agent_qa_write_test",
1351
+ note: "projected by 396 (inventory row 10, 2026-08-16): `authoring.draft.write` is workflow-coarse and verifies a draft rather than performing v1's blind write, so the input shape differs. Since 2026-10-02 (432-40, D-06) it writes the test file in the call unless a keep-gate rejected the draft."
1352
+ }),
1353
+ // 11
1354
+ Object.freeze({
1355
+ v1Name: "agent_qa_update_test",
1356
+ arm: "renamed-to",
1357
+ v2Tool: "agent_qa_refine_test",
1358
+ note: "projected by 396 (inventory row 11, 2026-08-16): `authoring.draft.edit` is the edit arm of the refine fold, selected by the fold discriminant."
1359
+ }),
1360
+ // 12
1361
+ Object.freeze({
1362
+ v1Name: "agent_qa_delete_test",
1363
+ arm: "retired",
1364
+ note: "not-a-gap (397, 2026-08-16): a rule, not an absence. MCPS-02 forbids a surface delete verb for irreversible deletes.",
1365
+ route: "removing a committed test is a repository edit a person makes and reviews; no surface has a delete door and none is coming."
1366
+ }),
1367
+ // 13
1368
+ Object.freeze({
1369
+ v1Name: "agent_qa_list_suites",
1370
+ arm: "pending",
1371
+ note: `gap (397, 2026-08-16): folded into row 7's verb \u2014 enumerating the corpus answers "which tests exist, and in which files". A suite is a file, so this is not a second registry.`,
1372
+ route: "until `entities.list` lands, suite membership is read from the workspace files themselves; the TST records inside them resolve through `agent_qa_get_entity`.",
1373
+ owner: "M5 (live view & run invocation)"
1374
+ }),
1375
+ // 14
1376
+ Object.freeze({
1377
+ v1Name: "agent_qa_read_suite",
1378
+ arm: "retired",
1379
+ note: "not-a-gap (397, 2026-08-16): there is no suite RECORD to read. `ENTITY_KINDS` has no suite kind and a suite is a committed file addressed by workspace path.",
1380
+ route: "the TST records inside a suite are `agent_qa_get_entity`'s job; the file itself is read from the workspace."
1381
+ }),
1382
+ // 15
1383
+ Object.freeze({
1384
+ v1Name: "agent_qa_validate_suite",
1385
+ arm: "retired",
1386
+ note: "not-a-gap (397, 2026-08-16): row 4's ruling \u2014 one validator, inside the funnel.",
1387
+ route: "the verify arm of `agent_qa_refine_test` validates the draft that joins the suite file."
1388
+ }),
1389
+ // 16
1390
+ Object.freeze({
1391
+ v1Name: "agent_qa_create_suite",
1392
+ arm: "retired",
1393
+ note: "not-a-gap (397, 2026-08-16): a suite is a committed file the authoring funnel writes. A blind create is the v1 write the verified authoring funnel replaced (re-spelled 2026-10-02, 432-40, D-06: the funnel writes in the call).",
1394
+ route: "`agent_qa_write_test` names the suite file a draft joins and writes it in the call, unless a keep-gate rejected the draft."
1395
+ }),
1396
+ // 17
1397
+ Object.freeze({
1398
+ v1Name: "agent_qa_update_suite",
1399
+ arm: "retired",
1400
+ note: "not-a-gap (397, 2026-08-16): row 16's ruling, through the edit arm instead of the write (re-spelled 2026-10-02, 432-40, D-06: the edit lands in the call).",
1401
+ route: "`agent_qa_refine_test` edits the written test; the suite file changes in the call."
1402
+ }),
1403
+ // 18
1404
+ Object.freeze({
1405
+ v1Name: "agent_qa_delete_suite",
1406
+ arm: "retired",
1407
+ note: "not-a-gap (397, 2026-08-16): row 12's MCPS-02 rule, applied to the file rather than the record.",
1408
+ route: "removing a suite file is a repository edit a person makes and reviews; no surface door exists."
1409
+ }),
1410
+ // 19
1411
+ Object.freeze({
1412
+ v1Name: "agent_qa_list_hooks",
1413
+ arm: "pending",
1414
+ note: "gap (397, 2026-08-16, corrected the same day): the 396 reason overstated `config.get`, which projects the resolved CONFIG's leaves; hook definitions live in the separate `workspace.hooksFile`, so no registered op reads them today.",
1415
+ route: "until the owning milestone re-opens the hook execution model, hooks are read from `workspace.hooksFile` in the workspace; no tool enumerates them.",
1416
+ owner: "M7 (skills layer)"
1417
+ }),
1418
+ // 20
1419
+ Object.freeze({
1420
+ v1Name: "agent_qa_read_hook",
1421
+ arm: "pending",
1422
+ note: "gap (397, 2026-08-16): with row 19, ONE hook-read verb and not two. What `config.get` shows is `workspace.hooksFile` \u2014 the path, not the hooks.",
1423
+ route: "until that verb lands, a single hook is read from the hooks file itself; no tool projects it.",
1424
+ owner: "M7 (skills layer)"
1425
+ }),
1426
+ // 21
1427
+ Object.freeze({
1428
+ v1Name: "agent_qa_create_hook",
1429
+ arm: "renamed-to",
1430
+ v2Tool: "agent_qa_change_config",
1431
+ note: "projected by 396 (inventory row 21, 2026-08-16): `config.hook.add` is an arm of the config fold. Since 2026-09-30 (432-12, D-05) the fold is `agent_qa_change_config` and the hook is written in the call, with the files it changed on the answer."
1432
+ }),
1433
+ // 22
1434
+ Object.freeze({
1435
+ v1Name: "agent_qa_update_hook",
1436
+ arm: "pending",
1437
+ note: "gap (397, 2026-08-16): `config.field.set` reaches any schema-addressable CONFIG path and the hooks file is not one, so an existing hook has no edit verb. Minting one now would pin an edit shape to a contract the owning milestone revisits.",
1438
+ route: "adding a hook goes through `agent_qa_change_config`; editing an existing one is a hooks-file edit a person makes until the owner rules on the verb.",
1439
+ owner: "M7 (skills layer)"
1440
+ }),
1441
+ // 23
1442
+ Object.freeze({
1443
+ v1Name: "agent_qa_delete_hook",
1444
+ arm: "retired",
1445
+ note: "not-a-gap (397, 2026-08-16): row 12's MCPS-02 rule.",
1446
+ route: "removing a hook is an edit to the hooks file that a person makes and reviews."
1447
+ }),
1448
+ // 24
1449
+ Object.freeze({
1450
+ v1Name: "agent_qa_run_hook",
1451
+ arm: "retired",
1452
+ note: `not-a-gap (397, 2026-08-16): a hook's whole contract is "runs at this lifecycle point". Invoking one by name from a surface is arbitrary script execution decoupled from the lifecycle that gives it meaning \u2014 the private-power shape M4 abolished.`,
1453
+ route: "hooks run at their declared lifecycle points during a run started by `agent_qa_start_run`; nothing invokes one out of band."
1454
+ }),
1455
+ // 25
1456
+ Object.freeze({
1457
+ v1Name: "agent_qa_enqueue_test_run",
1458
+ arm: "renamed-to",
1459
+ v2Tool: "agent_qa_start_run",
1460
+ note: "projected by 396 (inventory row 25, 2026-08-16): `runs.start` is workflow-coarse, and the test scope is an argument rather than a tool of its own."
1461
+ }),
1462
+ // 26
1463
+ Object.freeze({
1464
+ v1Name: "agent_qa_enqueue_suite_run",
1465
+ arm: "renamed-to",
1466
+ v2Tool: "agent_qa_start_run",
1467
+ note: "projected by 396 (inventory row 26, 2026-08-16): the SAME op as row 25 at suite scope, which is why D-01 allows one tool over both."
1468
+ }),
1469
+ // 27
1470
+ Object.freeze({
1471
+ v1Name: "agent_qa_get_run",
1472
+ arm: "retired",
1473
+ note: "A4 adjudication (2026-08-17, phase 398 plan 02): the inventory rules this row `n/a -> projected by 396`, but `runs.get` is in UNPROJECTED_OPS after 396 plan 04 \u2014 not a mid-flight workflow read per D-02 \u2014 so there is no live tool to rename to.",
1474
+ route: "the entity door renders a run's card: `agent_qa_get_entity` on the run reference. `agent_qa_list_runs` enumerates recent runs."
1475
+ }),
1476
+ // 28
1477
+ Object.freeze({
1478
+ v1Name: "agent_qa_get_run_steps",
1479
+ arm: "retired",
1480
+ note: "A4 adjudication (2026-08-17, phase 398 plan 02): ruled `n/a -> projected by 396` against `runs.get`/`runs.events`, both of which are in UNPROJECTED_OPS \u2014 `runs.events` is a stream op besides, and a `tools/call` is one round trip with no shape for a resumable feed.",
1481
+ route: "`agent_qa_get_run_digest` carries what triage needs from a run, including its steps' outcome; the run's card comes from `agent_qa_get_entity`."
1482
+ }),
1483
+ // 29
1484
+ Object.freeze({
1485
+ v1Name: "agent_qa_get_run_logs",
1486
+ arm: "retired",
1487
+ note: "A4 adjudication (2026-08-17, phase 398 plan 02): ruled `n/a -> projected by 396` against `runs.events`, which is in UNPROJECTED_OPS as a stream op with no single-round-trip shape.",
1488
+ route: "`agent_qa_get_run_digest` carries the triage-relevant remainder of a run; the full event feed is read from the run artifacts on disk, not over this surface."
1489
+ }),
1490
+ // 30
1491
+ Object.freeze({
1492
+ v1Name: "agent_qa_get_run_execution_logs",
1493
+ arm: "retired",
1494
+ note: "A4 adjudication (2026-08-17, phase 398 plan 02): row 29's reading \u2014 `runs.events` is unprojected, so the second logs alias has no live tool either.",
1495
+ route: "`agent_qa_get_run_digest` for triage; execution logs live in the run's artifacts on disk."
1496
+ }),
1497
+ // 31
1498
+ Object.freeze({
1499
+ v1Name: "agent_qa_get_run_artifact",
1500
+ arm: "pending",
1501
+ note: "gap (397, 2026-08-16): artifact bytes over a tool result is a disclosure surface nobody has scoped, so the read is routed WITH that scoping rather than minted without it \u2014 an artifact read without it is the gap made worse.",
1502
+ route: "until the owning milestone ships artifact resolution, artifacts are read from the run directory on disk; `agent_qa_get_run_digest` names what a run produced.",
1503
+ owner: "M5 (artifact resolution route)"
1504
+ }),
1505
+ // 32
1506
+ Object.freeze({
1507
+ v1Name: "agent_qa_cancel_run",
1508
+ arm: "pending",
1509
+ note: "gap (397, 2026-08-16): ruled a REAL gap as `runs.cancel` and routed to its owner rather than minted in 397, because D-17 binds the adjudicator too.",
1510
+ route: "until `runs.cancel` lands, a run is stopped by the host that started it; this surface starts runs through `agent_qa_start_run` and cannot stop one.",
1511
+ owner: "M5 (live-view control arbiter)"
1512
+ }),
1513
+ // 33
1514
+ Object.freeze({
1515
+ v1Name: "agent_qa_classify_failure",
1516
+ arm: "retired",
1517
+ note: "not-a-gap (397, 2026-08-16): classification on demand over dashboard-fetched data is the private data path M4 abolished, not a capability that went missing.",
1518
+ route: "`analysis.get` reads the verdict the `analyze-run` job produced; over this surface the triage answer is `agent_qa_get_run_digest`."
1519
+ })
1520
+ ]);
1521
+
1522
+ // src/projection.ts
1523
+ var JSON_SCHEMA_CONVERSION_TARGET = "draft-2020-12";
1524
+ function isReadOnly(specs) {
1525
+ return specs.every((spec) => spec.risk.readOnly === true);
282
1526
  }
283
- function classifyByNeedle(haystack, evidence, needles, category, confidence) {
284
- const match = needles.find((needle) => haystack.includes(needle));
285
- if (!match) return null;
286
- const matchingEvidence = evidence.find((item) => item.toLowerCase().includes(match)) ?? evidence[0] ?? match;
1527
+ function deriveAnnotations(specs) {
1528
+ const readOnlyHint = isReadOnly(specs);
287
1529
  return {
288
- category,
289
- confidence,
290
- evidence: [matchingEvidence].filter(Boolean).slice(0, 5),
291
- recentRelatedCount: 0
1530
+ readOnlyHint,
1531
+ destructiveHint: specs.some((spec) => spec.risk.destructive === true),
1532
+ idempotentHint: readOnlyHint
292
1533
  };
293
1534
  }
294
- function classifyRunFailureFromDashboardData(input) {
295
- const run = input.runDetail && typeof input.runDetail === "object" ? input.runDetail.run : void 0;
296
- const status = run && typeof run === "object" ? run.status : void 0;
297
- const evidence = collectStrings([input.runDetail, input.artifact, input.logs, input.executionLogs]);
298
- const haystack = evidence.join("\n").toLowerCase();
299
- const recentRelatedCount = Array.isArray(input.recentRuns?.runs) ? input.recentRuns.runs.length : 0;
300
- if (status && status !== "failed") {
301
- return {
302
- category: "passed",
303
- confidence: 0.95,
304
- evidence: [`Run status is ${String(status)}.`],
305
- recentRelatedCount
306
- };
1535
+ function describe(projection, specs) {
1536
+ const composition = compositionOf(projection);
1537
+ if (composition === "single") return specs[0].summary;
1538
+ const arms = specs.map((spec) => `${spec.id}: ${spec.summary}`).join("\n");
1539
+ if (composition === "pipeline") {
1540
+ return `${arms}
1541
+ These run in the order listed; the second runs only when the first resolves.`;
307
1542
  }
308
- const checks = [
309
- [["timed out", "timeout"], "timeout", 0.9],
310
- [["appium server failed", "appium"], "appium_startup", 0.9],
311
- [["browser closed", "target page, context or browser has been closed", "browser disconnect"], "browser_disconnect", 0.85],
312
- [["hook_not_runnable", "hook failed", "hook registry", "hook"], "hook_failure", 0.8],
313
- [["not found", "no element", "strict mode violation", "selector"], "element_not_found", 0.8],
314
- [["assert", "expected", "verify"], "assertion_failure", 0.75],
315
- [["econnrefused", "enotfound", "network", "docker"], "infrastructure", 0.7]
316
- ];
317
- for (const [needles, category, confidence] of checks) {
318
- const result = classifyByNeedle(haystack, evidence, needles, category, confidence);
319
- if (result) return { ...result, recentRelatedCount };
1543
+ return `${arms}
1544
+ Select one with "${projection.discriminant}", whose value is the op id above.`;
1545
+ }
1546
+ function degradationSentence(degradation) {
1547
+ return `
1548
+ This tool advertises no outputSchema: its result contract (${degradation.schema}) contains a construct JSON Schema cannot represent, and a narrowed stand-in would describe a shape nothing produces. The result still arrives as structuredContent.`;
1549
+ }
1550
+ function probeOutputSchema(projection, output) {
1551
+ const declared = DEGRADED_OUTPUT_CONTRACTS.find((entry) => entry.tool === projection.name);
1552
+ let failure;
1553
+ try {
1554
+ output["~standard"].jsonSchema.output({ target: JSON_SCHEMA_CONVERSION_TARGET });
1555
+ } catch (err) {
1556
+ failure = err instanceof Error ? err.message : String(err);
1557
+ }
1558
+ if (failure === void 0) {
1559
+ if (declared === void 0) return output;
1560
+ throw new McpProjectionError(
1561
+ projection.name,
1562
+ `is listed in DEGRADED_OUTPUT_CONTRACTS for ${declared.schema}, but that contract now converts cleanly in output mode; remove the entry so the schema is advertised again`
1563
+ );
1564
+ }
1565
+ if (declared === void 0) {
1566
+ throw new McpProjectionError(
1567
+ projection.name,
1568
+ `advertises an output contract JSON Schema cannot represent (${failure}). Declare it in DEGRADED_OUTPUT_CONTRACTS with the measured message, or give the op a contract that converts \u2014 an unrepresentable outputSchema throws inside tools/list and takes the whole listing down for every caller`
1569
+ );
1570
+ }
1571
+ return void 0;
1572
+ }
1573
+ function toRegisterToolConfig(projection, specs, advertised, maskText) {
1574
+ if (specs.length === 0) {
1575
+ throw new McpProjectionError(
1576
+ projection.name,
1577
+ "projects no ops; a tool with no arm has nothing to validate against and nothing to call"
1578
+ );
320
1579
  }
1580
+ if (specs.length !== projection.ops.length) {
1581
+ throw new McpProjectionError(
1582
+ projection.name,
1583
+ `declares ${projection.ops.length} ops but was derived from ${specs.length}`
1584
+ );
1585
+ }
1586
+ const outputSchema = probeOutputSchema(projection, advertised.output);
1587
+ const degradation = outputSchema === void 0 ? DEGRADED_OUTPUT_CONTRACTS.find((entry) => entry.tool === projection.name) : void 0;
1588
+ const description = describe(projection, specs);
1589
+ const meta = listingMeta(projection);
1590
+ const fold = compositionOf(projection) === "union";
1591
+ const inputSchema = fold ? flatFoldInput(projection, advertised.input) : advertised.input;
1592
+ if (fold) inputSchema["~standard"].jsonSchema.input({ target: JSON_SCHEMA_CONVERSION_TARGET });
321
1593
  return {
322
- category: "unknown_failure",
323
- confidence: 0.3,
324
- evidence: evidence.slice(0, 5),
325
- recentRelatedCount
1594
+ title: projection.name,
1595
+ description: maskText(
1596
+ degradation === void 0 ? description : description + degradationSentence(degradation)
1597
+ ),
1598
+ inputSchema,
1599
+ ...outputSchema === void 0 ? {} : { outputSchema },
1600
+ annotations: deriveAnnotations(specs),
1601
+ // 408 D-05: the App reference, DERIVED from the projection's own carrier declaration by the
1602
+ // same pipeline that derives the annotations above — never a pair of tool names written here.
1603
+ // The conditional spread is the house absent-field rule: a non-carrier's config has no `_meta`
1604
+ // key at all.
1605
+ //
1606
+ // NOT MASKED, and it joins the two exemptions the header names for their reason rather than by
1607
+ // analogy. `resourceUri` is the address constant this package publishes — a declared literal in
1608
+ // this repository's own source, exactly like the `title` above it — and a masker that rewrote
1609
+ // it would point every host at an address the resource codec refuses, which is the tool-rename
1610
+ // failure with a URI scheme attached. Nothing interpolated into it comes from a caller, from
1611
+ // storage or from a resolved value; the run id never appears here, because this is the
1612
+ // TEMPLATE and a per-call reference is composed downstream. The result-size hint (431 D-05) is
1613
+ // a declared constant too, and rides the same record (`listingMeta`).
1614
+ ...meta === void 0 ? {} : { _meta: meta }
1615
+ };
1616
+ }
1617
+ function listingMeta(projection) {
1618
+ const meta = {
1619
+ ...projection.ui === void 0 ? {} : { ui: { resourceUri: APP_RESOURCE_URI_TEMPLATE } },
1620
+ ...projection.maxResultSizeChars === void 0 ? {} : { [MAX_RESULT_SIZE_META_KEY]: projection.maxResultSizeChars }
326
1621
  };
1622
+ return Object.keys(meta).length === 0 ? void 0 : meta;
327
1623
  }
328
- function maskSensitive(value, parentKey = "") {
329
- const authRedacted = redactAuthStateValue(value);
330
- const lower = parentKey.toLowerCase();
331
- if (typeof authRedacted === "string" && authRedacted !== value) {
332
- return authRedacted;
1624
+ function flatFoldInput(projection, input) {
1625
+ const standard = input["~standard"];
1626
+ return {
1627
+ "~standard": {
1628
+ version: standard.version,
1629
+ vendor: standard.vendor,
1630
+ validate: (value, options) => standard.validate(value, options),
1631
+ jsonSchema: {
1632
+ input: (options) => flatFoldDocument(projection, standard.jsonSchema.input(options)),
1633
+ output: (options) => standard.jsonSchema.output(options)
1634
+ }
1635
+ }
1636
+ };
1637
+ }
1638
+ function flatFoldDocument(projection, document) {
1639
+ const key = projection.discriminant;
1640
+ const { oneOf, type: _type, ...rest } = document;
1641
+ const arms = Array.isArray(oneOf) ? oneOf : [];
1642
+ const unsupported = ["anyOf", "allOf", "properties"].filter((name) => rest[name] !== void 0);
1643
+ if (arms.length !== projection.ops.length || unsupported.length > 0) {
1644
+ throw new McpProjectionError(
1645
+ projection.name,
1646
+ `has an input contract that is not a ${projection.ops.length}-arm union of objects, so it cannot be listed as one object`
1647
+ );
333
1648
  }
334
- if (typeof authRedacted === "string" && (lower.includes("key") || lower.includes("token") || lower.includes("secret") || lower.includes("password"))) {
335
- return authRedacted.length > 8 ? `${authRedacted.slice(0, 3)}****${authRedacted.slice(-4)}` : "****";
1649
+ const ops = [];
1650
+ const fields = /* @__PURE__ */ new Map();
1651
+ let actionNote;
1652
+ let strict = true;
1653
+ for (const arm of arms) {
1654
+ const properties2 = arm.properties ?? {};
1655
+ const opId = properties2[key]?.const;
1656
+ if (arm.type !== "object" || typeof opId !== "string" || !projection.ops.includes(opId)) {
1657
+ throw new McpProjectionError(
1658
+ projection.name,
1659
+ `has a union arm that is not an object selected by a declared ${key}`
1660
+ );
1661
+ }
1662
+ ops.push(opId);
1663
+ if (arm.additionalProperties !== false) strict = false;
1664
+ actionNote ??= properties2[key]?.description;
1665
+ const required2 = arm.required ?? [];
1666
+ for (const [name, schema] of Object.entries(properties2)) {
1667
+ if (name === key) continue;
1668
+ const field = fields.get(name) ?? { shapes: [], notes: [], usedBy: [], requiredBy: [] };
1669
+ const { description, ...shape } = schema;
1670
+ if (!field.shapes.some((seen) => JSON.stringify(seen) === JSON.stringify(shape))) {
1671
+ field.shapes.push(shape);
1672
+ }
1673
+ if (typeof description === "string" && !field.notes.includes(description)) {
1674
+ field.notes.push(description);
1675
+ }
1676
+ field.usedBy.push(opId);
1677
+ if (required2.includes(name)) field.requiredBy.push(opId);
1678
+ fields.set(name, field);
1679
+ }
336
1680
  }
337
- if (Array.isArray(authRedacted)) return authRedacted.map((item) => maskSensitive(item, parentKey));
338
- if (authRedacted && typeof authRedacted === "object") {
339
- return Object.fromEntries(
340
- Object.entries(authRedacted).map(([key, child]) => [key, maskSensitive(child, key)])
1681
+ const perAction = ops.map((opId) => {
1682
+ const taken = [...fields].filter(([, field]) => field.usedBy.includes(opId));
1683
+ const listed = taken.map(
1684
+ ([name, field]) => field.requiredBy.includes(opId) ? name : `${name} (optional)`
341
1685
  );
1686
+ return `${opId} takes ${listed.length === 0 ? "no other field" : listed.join(", ")}.`;
1687
+ });
1688
+ const properties = {
1689
+ [key]: {
1690
+ type: "string",
1691
+ enum: ops,
1692
+ description: [actionNote, ...perAction].filter(Boolean).join(" ")
1693
+ }
1694
+ };
1695
+ const required = [key];
1696
+ for (const [name, field] of fields) {
1697
+ const scope = field.usedBy.length === ops.length ? "every action" : `${field.usedBy.join(" or ")} only`;
1698
+ const need = field.requiredBy.length === 0 ? "optional" : field.requiredBy.length === field.usedBy.length ? "required" : `required for ${field.requiredBy.join(" or ")}`;
1699
+ const shape = field.shapes.length === 1 ? field.shapes[0] : { anyOf: field.shapes };
1700
+ const description = [...field.notes, `For ${scope}; ${need}.`].join(" ");
1701
+ properties[name] = { ...shape, description };
1702
+ if (field.requiredBy.length === ops.length) required.push(name);
342
1703
  }
343
- return authRedacted;
1704
+ return {
1705
+ ...rest,
1706
+ type: "object",
1707
+ properties,
1708
+ required,
1709
+ ...strict ? { additionalProperties: false } : {}
1710
+ };
344
1711
  }
345
- async function readConfig(configPath) {
346
- const path = resolve(configPath ?? "agent-qa.config.yaml");
347
- const content = await readFile(path, "utf-8");
348
- const raw = parseYaml(content) ?? {};
349
- const validation = AgentQaConfigSchema2.safeParse(raw);
350
- const record = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
351
- const registry = record.registry && typeof record.registry === "object" && !Array.isArray(record.registry) ? record.registry : {};
352
- const services = record.services && typeof record.services === "object" && !Array.isArray(record.services) ? record.services : {};
1712
+ var McpProjectionError = class extends Error {
1713
+ constructor(toolName, detail) {
1714
+ super(`${toolName} ${detail}`);
1715
+ this.toolName = toolName;
1716
+ this.name = "McpProjectionError";
1717
+ }
1718
+ };
1719
+
1720
+ // src/resources.ts
1721
+ import { ResourceTemplate as ResourceTemplate2 } from "@modelcontextprotocol/server";
1722
+ import { invokeFunction as invokeFunction2 } from "@vostride/agent-qa-agent";
1723
+ import {
1724
+ ENTITY_KINDS,
1725
+ ENTITY_URI_SEGMENTS,
1726
+ formatEntityUri,
1727
+ parseEntityUri
1728
+ } from "@vostride/agent-qa-schema";
1729
+ var ENTITIES_GET_OP_ID = "entities.get";
1730
+ var ENTITY_RESOURCE_MIME_TYPE = "application/json";
1731
+ function templateFor(kind) {
1732
+ const segment = ENTITY_URI_SEGMENTS[kind];
353
1733
  return {
354
- path,
355
- raw: maskSensitive(raw),
356
- valid: validation.success,
357
- issues: validation.success ? [] : validation.error.issues.map((issue) => ({
358
- path: issue.path.map(String).join(".") || "(root)",
359
- message: issue.message
360
- })),
361
- summary: {
362
- workspaceConfigured: Boolean(record.workspace),
363
- serviceKeys: Object.keys(services),
364
- targetNames: Object.keys(registry.targets ?? {}),
365
- deviceNames: Object.keys(registry.devices ?? {}),
366
- providerNames: Object.keys(registry.providers ?? {}),
367
- llmNames: Array.isArray(registry.llms) ? registry.llms.map((llm) => llm && typeof llm === "object" ? llm.name : void 0).filter((name) => typeof name === "string") : []
368
- }
1734
+ kind,
1735
+ segment,
1736
+ name: `agent_qa_entity_${segment}`,
1737
+ uriTemplate: `agentqa://${segment}/{id}`,
1738
+ title: `agent-qa ${segment} entity`,
1739
+ // Authored prose about a KIND, not about any entity's content — the same class of string as a
1740
+ // tool's curated title, so it is not masked. Nothing interpolated here comes from storage.
1741
+ description: `The entity card for one ${segment} addressed by its canonical agentqa:// URI (for example agentqa://${segment}/${kind}-001). Resolved through the entities read op.`
369
1742
  };
370
1743
  }
371
- function createAgentQaMcpServer(options = {}) {
372
- const server = new McpServer({
373
- name: "agent-qa",
374
- version: getAgentQaVersion()
375
- });
376
- const analyticsService = options.analyticsService ?? createAnalyticsService({ config: options.analyticsConfig });
377
- const analyticsTransport = options.transport ?? "stdio";
378
- const registerAnalyticsTool = (name, definition, handler) => server.registerTool(name, definition, (async (input, extra) => {
379
- const startedAt = Date.now();
380
- let result;
381
- let thrown;
1744
+ var ENTITY_RESOURCE_TEMPLATES = ENTITY_KINDS.map(templateFor);
1745
+ var EntityResourceUriError = class extends Error {
1746
+ constructor(detail) {
1747
+ super(`${detail} is not a canonical agentqa:// entity address`);
1748
+ this.name = "EntityResourceUriError";
1749
+ }
1750
+ };
1751
+ var EntityResourceReadError = class extends Error {
1752
+ constructor(message) {
1753
+ super(message);
1754
+ this.name = "EntityResourceReadError";
1755
+ }
1756
+ };
1757
+ function createEntityResourceReadHandler(deps) {
1758
+ return async (uri) => {
1759
+ const parsed = parseEntityUri(uri.href);
1760
+ if (parsed === void 0) {
1761
+ throw new EntityResourceUriError(deps.maskText(uri.href));
1762
+ }
1763
+ let card;
382
1764
  try {
383
- result = await handler(input, extra);
384
- return result;
1765
+ card = await deps.invoke(ENTITIES_GET_OP_ID, { ref: parsed.shortId });
385
1766
  } catch (err) {
386
- thrown = err;
387
- throw err;
388
- } finally {
389
- const isError = thrown !== void 0 || isErrorResult(result) || isDashboardErrorResult(result);
390
- const explicitRunId = extractExplicitCanonicalRunId(input);
391
- const enqueueRunId = !isError && (name === "agent_qa_enqueue_test_run" || name === "agent_qa_enqueue_suite_run") ? extractSuccessfulEnqueueRunId(result) : void 0;
392
- try {
393
- if (options.analyticsConfig?.analytics?.privacy !== true) {
394
- const standardProperties = options.analyticsStandardProperties ?? await resolveAnalyticsStandardProperties({ surface: "mcp" });
395
- const event = buildAnalyticsEvent({
396
- name: "agent-qa.mcp.tool.invoked",
397
- properties: {
398
- ...standardProperties,
399
- surface: standardProperties.surface ?? "mcp",
400
- tool_name: name,
401
- mcp_tool_category: categorizeMcpTool(name),
402
- mcp_tool_status: isError ? "error" : "success",
403
- duration_ms: Math.max(0, Date.now() - startedAt),
404
- mcp_transport: analyticsTransport,
405
- ...isError ? { mcp_error_category: categorizeMcpError(thrown ?? result) } : {},
406
- ...explicitRunId ?? enqueueRunId ? { run_id: explicitRunId ?? enqueueRunId } : {}
407
- }
408
- });
409
- if (options.analyticsService) {
410
- await analyticsService.capture(event).catch(() => {
411
- });
412
- } else {
413
- await captureAnalytics(event, {
414
- config: options.analyticsConfig,
415
- surface: "mcp"
416
- }).catch(() => {
417
- });
418
- }
1767
+ throw new EntityResourceReadError(
1768
+ deps.maskText(err instanceof Error ? err.message : String(err))
1769
+ );
1770
+ }
1771
+ const masked = maskJsonDocument(card, deps.maskText);
1772
+ if (masked === void 0) {
1773
+ throw new EntityResourceReadError(
1774
+ "This entity could not be returned: part of its card could not be masked in place, so the unmasked record was withheld rather than served."
1775
+ );
1776
+ }
1777
+ return {
1778
+ contents: [
1779
+ {
1780
+ // Re-FORMATTED through the codec rather than echoed: the address a caller reads back is
1781
+ // the canonical one, so `agentqa://memory/MEM-1` answers about `MEM-001` and says so.
1782
+ uri: formatEntityUri(parsed.kind, parsed.shortId),
1783
+ mimeType: ENTITY_RESOURCE_MIME_TYPE,
1784
+ text: masked.text
419
1785
  }
420
- } catch {
421
- }
1786
+ ]
1787
+ };
1788
+ };
1789
+ }
1790
+ function registerEntityResources(server, deps) {
1791
+ const declaration = deps.registry.list(deps.caller).find((candidate) => candidate.id === ENTITIES_GET_OP_ID);
1792
+ if (declaration === void 0) return [];
1793
+ const read = createEntityResourceReadHandler({
1794
+ // The wall, bound once: every template's read crosses `invokeFunction` with this host's
1795
+ // registry and this caller, so input validation and the binding check run exactly as they do
1796
+ // for a tool call.
1797
+ invoke: (opId, input) => invokeFunction2(deps.registry, opId, input, deps.caller),
1798
+ maskText: deps.maskText
1799
+ });
1800
+ for (const template of ENTITY_RESOURCE_TEMPLATES) {
1801
+ server.registerResource(
1802
+ template.name,
1803
+ // `list: undefined` is REQUIRED to be spelled out by the SDK, and the value is the honest
1804
+ // one: enumerating every entity of a kind is a listing this surface does not offer, and the
1805
+ // SDK asks for the explicit `undefined` precisely so that absence is a decision.
1806
+ new ResourceTemplate2(template.uriTemplate, { list: void 0 }),
1807
+ // v2 requires the metadata argument; it is not optional the way it was in v1.
1808
+ {
1809
+ title: template.title,
1810
+ description: template.description,
1811
+ mimeType: ENTITY_RESOURCE_MIME_TYPE
1812
+ },
1813
+ read
1814
+ );
1815
+ }
1816
+ return ENTITY_RESOURCE_TEMPLATES;
1817
+ }
1818
+
1819
+ // src/catalog.ts
1820
+ var AGENT_QA_MCP_SERVER_VERSION = "2.0.0";
1821
+ function resolveSpecs(projection, declarations) {
1822
+ const specs = [];
1823
+ for (const opId of projection.ops) {
1824
+ const resolved = declarations.get(opId);
1825
+ if (resolved === void 0) return void 0;
1826
+ specs.push(resolved);
1827
+ }
1828
+ return specs;
1829
+ }
1830
+ function selectFoldArm(projection, args) {
1831
+ const key = projection.discriminant;
1832
+ const record = args ?? {};
1833
+ const selected = record[key];
1834
+ if (typeof selected !== "string" || !projection.ops.includes(selected)) {
1835
+ throw new McpProjectionError(
1836
+ projection.name,
1837
+ `was called with ${key}=${JSON.stringify(selected)}, which is not one of the ops it declares`
1838
+ );
1839
+ }
1840
+ const { [key]: _arm, ...input } = record;
1841
+ return { opId: selected, input };
1842
+ }
1843
+ function resolvedCardId(resolution) {
1844
+ if (typeof resolution !== "object" || resolution === null) return void 0;
1845
+ const record = resolution;
1846
+ if (record.kind !== "resolved") return void 0;
1847
+ const card = record.card;
1848
+ return typeof card?.id === "string" ? card.id : void 0;
1849
+ }
1850
+ async function runProjection(projection, args, invoke) {
1851
+ switch (compositionOf(projection)) {
1852
+ case "single":
1853
+ return { opId: projection.ops[0], output: await invoke(projection.ops[0], args) };
1854
+ case "union": {
1855
+ const arm = selectFoldArm(projection, args);
1856
+ return { opId: arm.opId, output: await invoke(arm.opId, arm.input) };
422
1857
  }
423
- }));
424
- const dashboardUrlField = z.string().url().optional().describe("Dashboard base URL; defaults to the MCP server dashboard URL.");
425
- const dashboardUrlFor = (inputUrl) => {
426
- const dashboardUrl = inputUrl ?? options.dashboardUrl;
427
- if (!dashboardUrl) {
428
- throw new Error("dashboardUrl is required for authoring tools. Start MCP via `agent-qa dashboard` or pass dashboardUrl.");
1858
+ case "pipeline": {
1859
+ const resolution = await invoke(projection.ops[0], args);
1860
+ const id = resolvedCardId(resolution);
1861
+ if (id === void 0) return { opId: projection.ops[0], output: resolution };
1862
+ const card = await invoke(projection.ops[1], { ref: id });
1863
+ return { opId: projection.ops[1], output: { ...resolution, card } };
429
1864
  }
430
- return dashboardUrl;
1865
+ }
1866
+ }
1867
+ function carrierRunIdOf(projection, args, output) {
1868
+ const source = projection.ui?.runIdFrom === "output" ? output : args;
1869
+ const runId = source?.runId;
1870
+ return typeof runId === "string" && runId !== "" ? runId : void 0;
1871
+ }
1872
+ function decorateCarrierResult(input) {
1873
+ if (input.projection.ui === void 0) return input.result;
1874
+ if (input.result.isError === true) return input.result;
1875
+ const runId = carrierRunIdOf(input.projection, input.args, input.output);
1876
+ const line = liveLinkLineFor(input.liveLink?.dashboardBaseUrl(), runId);
1877
+ return {
1878
+ ...input.result,
1879
+ content: [...input.result.content, { type: "text", text: input.maskText(line) }],
1880
+ ...runId === void 0 ? {} : { _meta: { ...input.result._meta, ui: { resourceUri: appResourceUriFor(runId) } } }
431
1881
  };
432
- const dashboardTool = async (dashboardUrl, path, method, body) => {
1882
+ }
1883
+ function createProjectionHandler(projection, invoke, maskText, liveLink) {
1884
+ return async (args) => {
433
1885
  try {
434
- const result = await callDashboardApi({
435
- dashboardUrl: dashboardUrlFor(dashboardUrl),
436
- path,
437
- method,
438
- body
1886
+ const { opId, output } = await runProjection(projection, args, invoke);
1887
+ const masked = maskedToolResult(output, maskText);
1888
+ const result = opId === RUNS_ARTIFACT_FUNCTION_ID ? decorateRunEvidenceImage({ result: masked, output, maskText }) : masked;
1889
+ return decorateCarrierResult({
1890
+ result,
1891
+ projection,
1892
+ args,
1893
+ output,
1894
+ maskText,
1895
+ ...liveLink === void 0 ? {} : { liveLink }
439
1896
  });
440
- return jsonContent(redactAuthStateValue(result));
441
1897
  } catch (err) {
442
- return errorContent(err instanceof Error ? err.message : String(err));
1898
+ return maskedToolError(err, maskText);
443
1899
  }
444
1900
  };
445
- registerAnalyticsTool(
446
- "agent_qa_discover",
447
- {
448
- title: "Discover agent-qa MCP capabilities",
449
- description: "Return structured metadata for agent-qa MCP tools, resources, prompts, and current rollout scope.",
450
- inputSchema: {}
451
- },
452
- async () => jsonContent({
453
- ...DISCOVERY,
454
- endpoint: options.endpointUrl,
455
- dashboard: options.dashboardUrl,
456
- configPath: options.configPath
457
- })
458
- );
459
- registerAnalyticsTool(
460
- "agent_qa_get_config",
461
- {
462
- title: "Inspect agent-qa config",
463
- description: "Read the active agent-qa config and return a masked raw config plus targets/devices/providers summary.",
464
- inputSchema: {
465
- configPath: z.string().optional().describe("Config path; defaults to the MCP server config path or agent-qa.config.yaml"),
466
- section: z.enum(["raw", "summary"]).optional().describe("Return full masked raw config or summary only")
467
- }
468
- },
469
- async ({ configPath, section }) => {
470
- const config = await readConfig(configPath ?? options.configPath);
471
- return jsonContent(section === "summary" ? { path: config.path, valid: config.valid, issues: config.issues, summary: config.summary } : config);
1901
+ }
1902
+ var TOOLS_CALL_METHOD = "tools/call";
1903
+ function callToolName(request) {
1904
+ if (typeof request !== "object" || request === null) return void 0;
1905
+ const params = request.params;
1906
+ if (typeof params !== "object" || params === null) return void 0;
1907
+ const name = params.name;
1908
+ return typeof name === "string" ? name : void 0;
1909
+ }
1910
+ function v1MigrationEntryFor(toolName) {
1911
+ return V1_TOOL_MIGRATION_MAP.find((entry) => entry.v1Name === toolName);
1912
+ }
1913
+ function v1RedirectFor(toolName, registered) {
1914
+ if (registered.has(toolName)) return void 0;
1915
+ return v1MigrationEntryFor(toolName);
1916
+ }
1917
+ function v1InputShapeNote(projection) {
1918
+ switch (compositionOf(projection)) {
1919
+ case "single":
1920
+ return "it takes one input object";
1921
+ case "union":
1922
+ return `set \`${projection.discriminant}\` to one of ${projection.ops.map((op) => `\`${op}\``).join(", ")} and pass that arm's own fields`;
1923
+ case "pipeline":
1924
+ return "it takes one entity reference and answers the card that reference resolves to";
1925
+ }
1926
+ }
1927
+ function v1RedirectMessage(entry, visibleTarget) {
1928
+ const opening = `\`${entry.v1Name}\` is a retired agent-qa v1 tool name and is not served.`;
1929
+ if (entry.arm === "renamed-to") {
1930
+ if (visibleTarget === void 0) {
1931
+ return `${opening} Call \`tools/list\` for the tools you may call on this server, and \`agent-qa init --agent\` for this workspace's capability manifest, which carries the v1 tool-name migration map.`;
472
1932
  }
473
- );
474
- registerAnalyticsTool(
475
- "agent_qa_schema_reference",
476
- {
477
- title: "Get agent-qa schema reference",
478
- description: "Return structured references for config, test YAML, suite YAML, hooks, or canonical IDs.",
479
- inputSchema: {
480
- schema: z.enum(SCHEMA_REFERENCE_NAMES).describe("Reference name to return")
481
- }
482
- },
483
- async ({ schema }) => jsonContent(AGENT_QA_SCHEMA_REFERENCES[schema])
484
- );
485
- registerAnalyticsTool(
486
- "agent_qa_validate_definition",
487
- {
488
- title: "Validate agent-qa definition",
489
- description: "Validate config/test/suite/hooks objects with the same core schemas agent-qa uses.",
490
- inputSchema: {
491
- kind: z.enum(VALIDATION_KINDS).describe("Definition kind"),
492
- definition: z.unknown().describe("Parsed config/test/suite/hooks object")
493
- }
494
- },
495
- async ({ kind, definition }) => jsonContent(validateAgentQaDefinition(kind, definition))
496
- );
497
- registerAnalyticsTool(
498
- "agent_qa_generate_id",
1933
+ return `${opening} The capability it named is now \`${visibleTarget.name}\`. ${entry.note} Its input shape is not v1's \u2014 ${v1InputShapeNote(visibleTarget)}; \`tools/list\` advertises the contract the call is validated against.`;
1934
+ }
1935
+ if (entry.arm === "pending") {
1936
+ const owned = entry.owner === void 0 ? "" : ` The capability it named is a known gap owned by ${entry.owner}.`;
1937
+ const until = entry.route === void 0 ? "" : ` Until then: ${entry.route}`;
1938
+ return `${opening}${owned} ${entry.note}${until}`;
1939
+ }
1940
+ return `${opening} ${entry.note}${entry.route === void 0 ? "" : ` Instead: ${entry.route}`}`;
1941
+ }
1942
+ function installV1NameRedirect(server, deps) {
1943
+ const dispatch = server.server;
1944
+ const sdkHandler = dispatch._requestHandlers?.get(TOOLS_CALL_METHOD);
1945
+ if (typeof sdkHandler !== "function") {
1946
+ throw new McpProjectionError(
1947
+ TOOLS_CALL_METHOD,
1948
+ "has no installed request handler to wrap; the SDK no longer registers tool dispatch where this package reads it, so the retired-v1-name redirect cannot be placed"
1949
+ );
1950
+ }
1951
+ dispatch.removeRequestHandler(TOOLS_CALL_METHOD);
1952
+ dispatch.setRequestHandler(TOOLS_CALL_METHOD, (request, ctx) => {
1953
+ const name = callToolName(request);
1954
+ const entry = name === void 0 ? void 0 : v1RedirectFor(name, deps.registered);
1955
+ if (entry === void 0) return sdkHandler(request, ctx);
1956
+ const target = entry.arm === "renamed-to" && entry.v2Tool !== void 0 && deps.registered.has(entry.v2Tool) ? AGENT_QA_TOOL_PROJECTIONS.find((projection) => projection.name === entry.v2Tool) : void 0;
1957
+ throw new ProtocolError(
1958
+ ProtocolErrorCode.InvalidParams,
1959
+ deps.maskText(v1RedirectMessage(entry, target))
1960
+ );
1961
+ });
1962
+ }
1963
+ function buildAgentQaMcpServer(deps) {
1964
+ const server = new McpServer(
1965
+ { name: "agent-qa", version: AGENT_QA_MCP_SERVER_VERSION },
499
1966
  {
500
- title: "Generate canonical agent-qa ID",
501
- description: "Generate a canonical id-agent backed ID for a test, suite, hook, run, or observation.",
502
- inputSchema: {
503
- type: z.enum(ENTITY_ID_TYPES).describe("ID entity type")
504
- }
505
- },
506
- async ({ type }) => {
507
- const idType = type;
508
- return jsonContent({
509
- id: generateCanonicalId(idType),
510
- type: idType,
511
- contract: getEntityIdContract(idType)
512
- });
1967
+ capabilities: {
1968
+ tools: {},
1969
+ resources: {},
1970
+ // 408 D-04: the official MCP Apps extension, ADVERTISED rather than assumed. Extensions are
1971
+ // bilateral opt-in — a client declares support in the `io.modelcontextprotocol/
1972
+ // clientCapabilities` envelope it sends on every request, and reads the server's set back
1973
+ // from discovery — so a server that renders App HTML without naming the extension here is
1974
+ // one no host will ever ask for it. The value is an empty record because this extension
1975
+ // carries no server-side options; what it says is that this endpoint speaks it at all.
1976
+ extensions: { [MCP_APPS_EXTENSION_ID]: {} }
1977
+ },
1978
+ // 433-33 (G-433-9): init's pointer, on the one channel every MCP host receives.
1979
+ instructions: AGENT_QA_MCP_INSTRUCTIONS
513
1980
  }
514
1981
  );
515
- registerAnalyticsTool(
516
- "agent_qa_validate_id",
517
- {
518
- title: "Validate canonical agent-qa ID",
519
- description: "Validate canonical id-agent backed IDs and return the expected contract.",
520
- inputSchema: {
521
- type: z.enum(ENTITY_ID_TYPES).describe("ID entity type"),
522
- id: z.string().describe("ID to validate")
523
- }
524
- },
525
- async ({ type, id }) => {
526
- const idType = type;
527
- const contract = getEntityIdContract(idType);
528
- const valid = isCanonicalId(idType, id);
529
- return jsonContent({
530
- valid,
531
- id,
532
- type: idType,
533
- contract,
534
- message: valid ? "ID is canonical." : `Expected ${contract.prefixWithSeparator} followed by ${contract.words} id-agent words.`
535
- });
1982
+ const invoke = (opId, input) => invokeFunction3(deps.registry, opId, input, deps.caller);
1983
+ const declarations = /* @__PURE__ */ new Map();
1984
+ for (const declaration of deps.registry.list(deps.caller)) {
1985
+ const contracts = AGENT_QA_OP_CONTRACTS[declaration.id];
1986
+ if (contracts === void 0) continue;
1987
+ declarations.set(declaration.id, {
1988
+ id: declaration.id,
1989
+ summary: declaration.summary,
1990
+ risk: declaration.risk,
1991
+ input: contracts.input,
1992
+ output: contracts.output
1993
+ });
1994
+ }
1995
+ const registered = /* @__PURE__ */ new Set();
1996
+ for (const projection of AGENT_QA_TOOL_PROJECTIONS) {
1997
+ if (registered.has(projection.name)) {
1998
+ throw new McpProjectionError(
1999
+ projection.name,
2000
+ "is declared twice in the mapping table; another projection already occupies that name"
2001
+ );
536
2002
  }
537
- );
538
- registerAnalyticsTool(
539
- "agent_qa_list_tests",
540
- {
541
- title: "List agent-qa tests",
542
- description: "List tests through the dashboard API using dashboard workspace discovery.",
543
- inputSchema: { dashboardUrl: dashboardUrlField }
544
- },
545
- async ({ dashboardUrl }) => dashboardTool(dashboardUrl, "/api/tests")
546
- );
547
- registerAnalyticsTool(
548
- "agent_qa_read_test",
549
- {
550
- title: "Read agent-qa test",
551
- description: "Read a test by canonical test ID through the dashboard API.",
552
- inputSchema: {
553
- dashboardUrl: dashboardUrlField,
554
- testId: z.string().describe("Canonical t_ test ID")
555
- }
556
- },
557
- async ({ dashboardUrl, testId }) => dashboardTool(dashboardUrl, `/api/tests/${encodeURIComponent(testId)}`)
558
- );
559
- registerAnalyticsTool(
560
- "agent_qa_validate_test",
561
- {
562
- title: "Validate agent-qa test YAML",
563
- description: "Validate test YAML through the dashboard test-file manager.",
564
- inputSchema: {
565
- dashboardUrl: dashboardUrlField,
566
- content: z.string().describe("Test YAML content"),
567
- filePath: z.string().optional().describe("Optional file path for suite/test routing hints")
568
- }
569
- },
570
- async ({ dashboardUrl, content, filePath }) => dashboardTool(dashboardUrl, "/api/tests/validate", "POST", { content, filePath })
571
- );
572
- registerAnalyticsTool(
573
- "agent_qa_create_test",
574
- {
575
- title: "Create agent-qa test",
576
- description: "Create a test file through the dashboard workspace-safe file manager.",
577
- inputSchema: {
578
- dashboardUrl: dashboardUrlField,
579
- path: z.string().describe("Workspace-relative test path matched by workspace.testMatch"),
580
- content: z.string().describe("Test YAML content")
581
- }
582
- },
583
- async ({ dashboardUrl, path, content }) => dashboardTool(dashboardUrl, "/api/tests", "POST", { path, content })
584
- );
585
- registerAnalyticsTool(
586
- "agent_qa_update_test",
587
- {
588
- title: "Update agent-qa test",
589
- description: "Update a test by canonical test ID through the dashboard API.",
590
- inputSchema: {
591
- dashboardUrl: dashboardUrlField,
592
- testId: z.string().describe("Canonical t_ test ID"),
593
- content: z.string().describe("Replacement test YAML content")
594
- }
595
- },
596
- async ({ dashboardUrl, testId, content }) => dashboardTool(dashboardUrl, `/api/tests/${encodeURIComponent(testId)}`, "PUT", { content })
597
- );
598
- registerAnalyticsTool(
599
- "agent_qa_delete_test",
600
- {
601
- title: "Delete agent-qa test",
602
- description: "Delete a test by canonical test ID through the dashboard API.",
603
- inputSchema: {
604
- dashboardUrl: dashboardUrlField,
605
- testId: z.string().describe("Canonical t_ test ID")
606
- }
607
- },
608
- async ({ dashboardUrl, testId }) => dashboardTool(dashboardUrl, `/api/tests/${encodeURIComponent(testId)}`, "DELETE")
609
- );
610
- registerAnalyticsTool(
611
- "agent_qa_list_suites",
612
- {
613
- title: "List agent-qa suites",
614
- description: "List suites through the dashboard API using dashboard workspace discovery.",
615
- inputSchema: { dashboardUrl: dashboardUrlField }
616
- },
617
- async ({ dashboardUrl }) => dashboardTool(dashboardUrl, "/api/suites")
618
- );
619
- registerAnalyticsTool(
620
- "agent_qa_read_suite",
621
- {
622
- title: "Read agent-qa suite",
623
- description: "Read a suite by canonical suite ID through the dashboard API.",
624
- inputSchema: {
625
- dashboardUrl: dashboardUrlField,
626
- suiteId: z.string().describe("Canonical s_ suite ID")
627
- }
628
- },
629
- async ({ dashboardUrl, suiteId }) => dashboardTool(dashboardUrl, `/api/suites/${encodeURIComponent(suiteId)}`)
630
- );
631
- registerAnalyticsTool(
632
- "agent_qa_validate_suite",
633
- {
634
- title: "Validate agent-qa suite YAML",
635
- description: "Validate suite YAML through the dashboard suite-file manager.",
636
- inputSchema: {
637
- dashboardUrl: dashboardUrlField,
638
- content: z.string().describe("Suite YAML content")
639
- }
640
- },
641
- async ({ dashboardUrl, content }) => dashboardTool(dashboardUrl, "/api/suites/validate", "POST", { content })
642
- );
643
- registerAnalyticsTool(
644
- "agent_qa_create_suite",
645
- {
646
- title: "Create agent-qa suite",
647
- description: "Create a suite file through the dashboard workspace-safe suite manager.",
648
- inputSchema: {
649
- dashboardUrl: dashboardUrlField,
650
- path: z.string().describe("Workspace-relative suite path matched by workspace.suiteMatch"),
651
- content: z.string().describe("Suite YAML content")
652
- }
653
- },
654
- async ({ dashboardUrl, path, content }) => dashboardTool(dashboardUrl, "/api/suites", "POST", { path, content })
655
- );
656
- registerAnalyticsTool(
657
- "agent_qa_update_suite",
658
- {
659
- title: "Update agent-qa suite",
660
- description: "Update a suite by canonical suite ID through the dashboard API.",
661
- inputSchema: {
662
- dashboardUrl: dashboardUrlField,
663
- suiteId: z.string().describe("Canonical s_ suite ID"),
664
- content: z.string().describe("Replacement suite YAML content")
665
- }
666
- },
667
- async ({ dashboardUrl, suiteId, content }) => dashboardTool(dashboardUrl, `/api/suites/${encodeURIComponent(suiteId)}`, "PUT", { content })
668
- );
669
- registerAnalyticsTool(
670
- "agent_qa_delete_suite",
671
- {
672
- title: "Delete agent-qa suite",
673
- description: "Delete a suite through the dashboard API. Prefer suiteId; path is supported for the dashboard delete route.",
674
- inputSchema: {
675
- dashboardUrl: dashboardUrlField,
676
- suiteId: z.string().optional().describe("Canonical s_ suite ID"),
677
- path: z.string().optional().describe("Workspace-relative suite path")
678
- }
679
- },
680
- async ({ dashboardUrl, suiteId, path }) => {
681
- if (path) return dashboardTool(dashboardUrl, `/api/suites/${encodeURIComponent(path)}`, "DELETE");
682
- if (!suiteId) return errorContent("Either suiteId or path is required.");
683
- let readResult;
684
- try {
685
- readResult = await callDashboardApi({
686
- dashboardUrl: dashboardUrlFor(dashboardUrl),
687
- path: `/api/suites/${encodeURIComponent(suiteId)}`
688
- });
689
- } catch (err) {
690
- return errorContent(err instanceof Error ? err.message : String(err));
2003
+ for (const opId of projection.ops) {
2004
+ if (AGENT_QA_OP_CONTRACTS[opId] === void 0) {
2005
+ throw new McpProjectionError(
2006
+ projection.name,
2007
+ `names op ${opId}, which has no entry in the contract table`
2008
+ );
691
2009
  }
692
- if (!readResult.ok || !readResult.body || typeof readResult.body !== "object") return jsonContent(readResult);
693
- const foundPath = readResult.body.path;
694
- if (typeof foundPath !== "string") return errorContent("Suite read response did not include a path.");
695
- return dashboardTool(dashboardUrl, `/api/suites/${encodeURIComponent(foundPath)}`, "DELETE");
696
2010
  }
697
- );
698
- registerAnalyticsTool(
699
- "agent_qa_list_hooks",
700
- {
701
- title: "List agent-qa hooks",
702
- description: "List hooks through the dashboard hook registry API.",
703
- inputSchema: { dashboardUrl: dashboardUrlField }
704
- },
705
- async ({ dashboardUrl }) => dashboardTool(dashboardUrl, "/api/hooks")
706
- );
707
- registerAnalyticsTool(
708
- "agent_qa_read_hook",
709
- {
710
- title: "Read agent-qa hook",
711
- description: "Read hook metadata and source by canonical hook ID.",
712
- inputSchema: {
713
- dashboardUrl: dashboardUrlField,
714
- hookId: z.string().describe("Canonical h_ hook ID")
715
- }
716
- },
717
- async ({ dashboardUrl, hookId }) => dashboardTool(dashboardUrl, `/api/hooks/${encodeURIComponent(hookId)}`)
718
- );
719
- registerAnalyticsTool(
720
- "agent_qa_create_hook",
721
- {
722
- title: "Create agent-qa hook",
723
- description: "Create a hook through the dashboard hook registry API.",
724
- inputSchema: {
725
- dashboardUrl: dashboardUrlField,
726
- payload: z.record(z.string(), z.unknown()).describe("Dashboard HookMutationRequest payload")
727
- }
728
- },
729
- async ({ dashboardUrl, payload }) => dashboardTool(dashboardUrl, "/api/hooks", "POST", payload)
730
- );
731
- registerAnalyticsTool(
732
- "agent_qa_update_hook",
733
- {
734
- title: "Update agent-qa hook",
735
- description: "Update a hook through the dashboard hook registry API.",
736
- inputSchema: {
737
- dashboardUrl: dashboardUrlField,
738
- hookId: z.string().describe("Canonical h_ hook ID"),
739
- payload: z.record(z.string(), z.unknown()).describe("Dashboard HookMutationRequest payload")
740
- }
741
- },
742
- async ({ dashboardUrl, hookId, payload }) => dashboardTool(dashboardUrl, `/api/hooks/${encodeURIComponent(hookId)}`, "PUT", payload)
743
- );
744
- registerAnalyticsTool(
745
- "agent_qa_delete_hook",
746
- {
747
- title: "Delete agent-qa hook",
748
- description: "Delete a hook through the dashboard hook registry API.",
749
- inputSchema: {
750
- dashboardUrl: dashboardUrlField,
751
- hookId: z.string().describe("Canonical h_ hook ID"),
752
- force: z.boolean().optional().describe("Delete even when references exist")
753
- }
754
- },
755
- async ({ dashboardUrl, hookId, force }) => dashboardTool(dashboardUrl, `/api/hooks/${encodeURIComponent(hookId)}${force ? "?force=true" : ""}`, "DELETE")
756
- );
757
- registerAnalyticsTool(
758
- "agent_qa_run_hook",
759
- {
760
- title: "Run agent-qa hook",
761
- description: "Run a hook through the dashboard hook execution API.",
762
- inputSchema: {
763
- dashboardUrl: dashboardUrlField,
764
- hookId: z.string().describe("Canonical h_ hook ID"),
765
- payload: z.record(z.string(), z.unknown()).optional().describe("Dashboard HookRunRequest payload")
766
- }
767
- },
768
- async ({ dashboardUrl, hookId, payload }) => dashboardTool(dashboardUrl, `/api/hooks/${encodeURIComponent(hookId)}/run`, "POST", payload ?? {})
769
- );
770
- registerAnalyticsTool(
771
- "agent_qa_enqueue_test_run",
772
- {
773
- title: "Enqueue agent-qa test run",
774
- description: "Queue a test run through the dashboard run trigger API and return the canonical run ID.",
775
- inputSchema: {
776
- dashboardUrl: dashboardUrlField,
777
- file: z.string().optional().describe("Workspace-relative test file path"),
778
- patterns: z.array(z.string()).optional().describe("Optional test patterns"),
779
- noCache: z.boolean().optional(),
780
- noMemory: z.boolean().optional(),
781
- local: z.boolean().optional()
782
- }
783
- },
784
- async ({ dashboardUrl, file, patterns, noCache, noMemory, local }) => dashboardTool(dashboardUrl, "/api/runs/trigger", "POST", { file, patterns, noCache, noMemory, local, triggerSource: "mcp" })
785
- );
786
- registerAnalyticsTool(
787
- "agent_qa_enqueue_suite_run",
788
- {
789
- title: "Enqueue agent-qa suite run",
790
- description: "Queue a suite run through the dashboard run trigger API and return parent run context.",
791
- inputSchema: {
792
- dashboardUrl: dashboardUrlField,
793
- file: z.string().describe("Workspace-relative suite file path"),
794
- noCache: z.boolean().optional(),
795
- noMemory: z.boolean().optional(),
796
- local: z.boolean().optional()
797
- }
798
- },
799
- async ({ dashboardUrl, file, noCache, noMemory, local }) => dashboardTool(dashboardUrl, "/api/runs/trigger", "POST", { file, noCache, noMemory, local, triggerSource: "mcp" })
800
- );
801
- registerAnalyticsTool(
802
- "agent_qa_get_run",
803
- {
804
- title: "Get agent-qa run",
805
- description: "Get run detail, steps, attempts, and suite child context through the dashboard API.",
806
- inputSchema: {
807
- dashboardUrl: dashboardUrlField,
808
- runId: z.string().describe("Canonical r_ run ID")
809
- }
810
- },
811
- async ({ dashboardUrl, runId }) => dashboardTool(dashboardUrl, `/api/runs/${encodeURIComponent(runId)}`)
812
- );
813
- registerAnalyticsTool(
814
- "agent_qa_get_run_steps",
815
- {
816
- title: "Get agent-qa run steps",
817
- description: "Get run steps through the dashboard API.",
818
- inputSchema: {
819
- dashboardUrl: dashboardUrlField,
820
- runId: z.string().describe("Canonical r_ run ID")
821
- }
822
- },
823
- async ({ dashboardUrl, runId }) => dashboardTool(dashboardUrl, `/api/runs/${encodeURIComponent(runId)}/steps`)
824
- );
825
- registerAnalyticsTool(
826
- "agent_qa_get_run_logs",
827
- {
828
- title: "Get agent-qa run logs",
829
- description: "Get run log rows through the dashboard API.",
830
- inputSchema: {
831
- dashboardUrl: dashboardUrlField,
832
- runId: z.string().describe("Canonical r_ run ID"),
833
- stepId: z.string().optional(),
834
- level: z.string().optional(),
835
- source: z.string().optional(),
836
- limit: z.number().int().positive().optional(),
837
- offset: z.number().int().min(0).optional()
838
- }
839
- },
840
- async ({ dashboardUrl, runId, stepId, level, source, limit, offset }) => dashboardTool(
841
- dashboardUrl,
842
- withQuery(`/api/runs/${encodeURIComponent(runId)}/logs`, { stepId, level, source, limit, offset })
843
- )
844
- );
845
- registerAnalyticsTool(
846
- "agent_qa_get_run_execution_logs",
847
- {
848
- title: "Get agent-qa execution logs",
849
- description: "Get structured execution logs through the dashboard API.",
850
- inputSchema: {
851
- dashboardUrl: dashboardUrlField,
852
- runId: z.string().describe("Canonical r_ run ID"),
853
- stepId: z.string().optional(),
854
- type: z.string().optional()
855
- }
856
- },
857
- async ({ dashboardUrl, runId, stepId, type }) => dashboardTool(
858
- dashboardUrl,
859
- withQuery(`/api/runs/${encodeURIComponent(runId)}/execution-logs`, { stepId, type })
860
- )
861
- );
862
- registerAnalyticsTool(
863
- "agent_qa_get_run_artifact",
864
- {
865
- title: "Get agent-qa run artifact bundle",
866
- description: "Get sanitized run artifact bundle, child artifacts, and missing artifact sections.",
867
- inputSchema: {
868
- dashboardUrl: dashboardUrlField,
869
- runId: z.string().describe("Canonical r_ run ID")
870
- }
871
- },
872
- async ({ dashboardUrl, runId }) => dashboardTool(dashboardUrl, `/api/runs/${encodeURIComponent(runId)}/artifact`)
873
- );
874
- registerAnalyticsTool(
875
- "agent_qa_cancel_run",
876
- {
877
- title: "Cancel agent-qa run",
878
- description: "Cancel a pending or running run through the dashboard queue.",
879
- inputSchema: {
880
- dashboardUrl: dashboardUrlField,
881
- runId: z.string().describe("Canonical r_ run ID")
882
- }
883
- },
884
- async ({ dashboardUrl, runId }) => dashboardTool(dashboardUrl, `/api/runs/${encodeURIComponent(runId)}/cancel`, "POST", {})
885
- );
886
- registerAnalyticsTool(
887
- "agent_qa_classify_failure",
888
- {
889
- title: "Classify agent-qa failure",
890
- description: "Classify a failed run using dashboard run detail, artifacts, logs, execution logs, and recent related runs.",
891
- inputSchema: {
892
- dashboardUrl: dashboardUrlField,
893
- runId: z.string().describe("Canonical r_ run ID")
894
- }
895
- },
896
- async ({ dashboardUrl, runId }) => {
897
- try {
898
- const baseUrl = dashboardUrlFor(dashboardUrl);
899
- const runDetail = await callDashboardApi({ dashboardUrl: baseUrl, path: `/api/runs/${encodeURIComponent(runId)}` });
900
- const artifact = await callDashboardApi({ dashboardUrl: baseUrl, path: `/api/runs/${encodeURIComponent(runId)}/artifact` });
901
- const logs = await callDashboardApi({ dashboardUrl: baseUrl, path: `/api/runs/${encodeURIComponent(runId)}/logs` });
902
- const executionLogs = await callDashboardApi({ dashboardUrl: baseUrl, path: `/api/runs/${encodeURIComponent(runId)}/execution-logs` });
903
- const runName = runDetail.body && typeof runDetail.body === "object" && runDetail.body.run && typeof runDetail.body.run?.name === "string" ? runDetail.body.run.name : void 0;
904
- const recentRuns = runName ? await callDashboardApi({ dashboardUrl: baseUrl, path: withQuery("/api/runs", { name: runName, limit: 5 }) }) : { ok: true, status: 200, body: { runs: [] } };
905
- return jsonContent({
906
- runId,
907
- classification: classifyRunFailureFromDashboardData({
908
- runDetail: runDetail.body,
909
- artifact: artifact.body,
910
- logs: logs.body,
911
- executionLogs: executionLogs.body,
912
- recentRuns: recentRuns.body
913
- }),
914
- sources: {
915
- run: { ok: runDetail.ok, status: runDetail.status },
916
- artifact: { ok: artifact.ok, status: artifact.status },
917
- logs: { ok: logs.ok, status: logs.status },
918
- executionLogs: { ok: executionLogs.ok, status: executionLogs.status },
919
- recentRuns: { ok: recentRuns.ok, status: recentRuns.status }
920
- }
921
- });
922
- } catch (err) {
923
- return errorContent(err instanceof Error ? err.message : String(err));
924
- }
2011
+ const advertised = advertisedContracts(projection);
2012
+ if (advertised === void 0) {
2013
+ throw new McpProjectionError(
2014
+ projection.name,
2015
+ "has no advertised contract; a multi-arm tool needs an entry in AGENT_QA_COMPOSED_CONTRACTS"
2016
+ );
925
2017
  }
926
- );
927
- for (const schema of SCHEMA_REFERENCE_NAMES) {
928
- server.registerResource(
929
- `agent_qa.schema.${schema}`,
930
- `agent-qa://schema/${schema}`,
931
- {
932
- title: `agent-qa ${schema} schema reference`,
933
- description: `Structured agent-qa reference for ${schema}.`,
934
- mimeType: "application/json"
935
- },
936
- async (uri) => ({
937
- contents: [{
938
- uri: uri.href,
939
- mimeType: "application/json",
940
- text: JSON.stringify(AGENT_QA_SCHEMA_REFERENCES[schema], null, 2)
941
- }]
942
- })
2018
+ const specs = resolveSpecs(projection, declarations);
2019
+ if (specs === void 0) continue;
2020
+ registered.add(projection.name);
2021
+ const config = toRegisterToolConfig(projection, specs, advertised, deps.maskText);
2022
+ server.registerTool(
2023
+ projection.name,
2024
+ config,
2025
+ createProjectionHandler(projection, invoke, deps.maskText, deps.liveLink)
943
2026
  );
944
2027
  }
945
- server.registerPrompt(
946
- "agent_qa_authoring_context",
947
- {
948
- title: "agent-qa authoring context",
949
- description: "Prompt context for agents writing agent-qa tests and suites.",
950
- argsSchema: {}
951
- },
952
- async () => ({
953
- messages: [{
954
- role: "user",
955
- content: {
956
- type: "text",
957
- text: [
958
- "Use agent-qa MCP tools before editing files.",
959
- "Call agent_qa_get_config to inspect targets/devices/providers.",
960
- "Call agent_qa_generate_id for test, suite, hook, run, and observation IDs.",
961
- "Call agent_qa_validate_definition before saving config, test, suite, or hooks YAML.",
962
- `Canonical ID contracts: ${JSON.stringify(getEntityIdContracts2())}`
963
- ].join("\n")
964
- }
965
- }]
966
- })
967
- );
2028
+ installV1NameRedirect(server, { registered, maskText: deps.maskText });
2029
+ registerEntityResources(server, {
2030
+ registry: deps.registry,
2031
+ caller: deps.caller,
2032
+ maskText: deps.maskText
2033
+ });
2034
+ registerAppResources(server, {
2035
+ registry: deps.registry,
2036
+ caller: deps.caller,
2037
+ maskText: deps.maskText,
2038
+ // 408 plan 03 (D-08): the SAME supplier the carrier decoration reads, so the anchor inside the
2039
+ // App document and the line beside a tool result name one page for one run.
2040
+ ...deps.liveLink === void 0 ? {} : { liveLink: deps.liveLink }
2041
+ });
968
2042
  return server;
969
2043
  }
970
2044
 
971
- // src/server.ts
972
- var MCP_STDIO_STARTUP_MESSAGE = "agent-qa MCP server running over stdio. Waiting for MCP client messages on stdin. Stdout is reserved for MCP protocol traffic.";
973
- function createMcpServer(options = {}) {
974
- return createAgentQaMcpServer(options);
975
- }
976
- async function startMcpServer(options = {}) {
977
- const { startupOutput, ...serverOptions } = options;
978
- const server = createMcpServer({ ...serverOptions, transport: "stdio" });
979
- const transport = new StdioServerTransport();
980
- await server.connect(transport);
981
- try {
982
- startupOutput?.write(`${MCP_STDIO_STARTUP_MESSAGE}
983
- `);
984
- } catch {
985
- }
986
- try {
987
- if (options.analyticsConfig?.analytics?.privacy !== true) {
988
- const standardProperties = options.analyticsStandardProperties ?? await resolveAnalyticsStandardProperties2({ surface: "mcp" });
989
- const event = buildAnalyticsEvent2({
990
- name: "agent-qa.mcp.server.lifecycle",
991
- properties: {
992
- ...standardProperties,
993
- surface: standardProperties.surface ?? "mcp",
994
- mcp_server_state: "started",
995
- mcp_transport: "stdio"
996
- }
997
- });
998
- if (options.analyticsService) {
999
- await options.analyticsService.capture(event).catch(() => {
1000
- });
1001
- } else {
1002
- await captureAnalytics2(event, {
1003
- config: options.analyticsConfig,
1004
- surface: "mcp"
1005
- }).catch(() => {
1006
- });
1007
- }
1008
- }
1009
- } catch {
1010
- }
2045
+ // src/http.ts
2046
+ import { createMcpHandler } from "@modelcontextprotocol/server";
2047
+ import { toNodeHandler } from "@modelcontextprotocol/node";
2048
+ function createAgentQaMcpNodeHandler(resolveDeps) {
2049
+ return toNodeHandler(createMcpHandler((ctx) => buildAgentQaMcpServer(resolveDeps(ctx))));
1011
2050
  }
1012
2051
 
1013
- // src/local-http.ts
1014
- import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
1015
- var DEFAULT_MCP_HOST = "127.0.0.1";
1016
- var DEFAULT_MCP_PORT = 3471;
1017
- var DEFAULT_MCP_PATH = "/mcp";
1018
- function normalizePath(path) {
1019
- if (!path) return DEFAULT_MCP_PATH;
1020
- return path.startsWith("/") ? path : `/${path}`;
2052
+ // src/conformance-support.ts
2053
+ function withoutDialect(document) {
2054
+ const { $schema: _dialect, ...rest } = document ?? {};
2055
+ return stripRedundantObjectStamp(rest);
1021
2056
  }
1022
- function jsonRpcError(res, status, message) {
1023
- res.writeHead(status, {
1024
- "Content-Type": "application/json",
1025
- "Access-Control-Allow-Origin": "*"
1026
- });
1027
- res.end(JSON.stringify({
1028
- jsonrpc: "2.0",
1029
- error: {
1030
- code: -32e3,
1031
- message
1032
- },
1033
- id: null
1034
- }));
2057
+ function stripRedundantObjectStamp(document) {
2058
+ const composed = document.oneOf !== void 0 || document.anyOf !== void 0;
2059
+ if (!composed || document.type !== "object") return document;
2060
+ const { type: _stamp, ...rest } = document;
2061
+ return rest;
1035
2062
  }
1036
- function resolveLocalMcpEndpoint(config = {}) {
1037
- const transport = config.transport ?? "http";
1038
- const host = config.host ?? DEFAULT_MCP_HOST;
1039
- const port = config.port ?? DEFAULT_MCP_PORT;
1040
- const path = normalizePath(config.path);
1041
- const displayHost = host === "::1" ? "[::1]" : host;
1042
- return {
1043
- enabled: config.enabled !== false,
1044
- transport,
1045
- host,
1046
- port,
1047
- path,
1048
- url: `http://${displayHost}:${port}${path}`
1049
- };
1050
- }
1051
- function resolveMcpEndpointShape(endpoint) {
1052
- return {
1053
- mcp_host_kind: ["127.0.0.1", "localhost", "::1"].includes(endpoint.host) ? "loopback" : "other",
1054
- mcp_port_kind: endpoint.port === DEFAULT_MCP_PORT ? "default" : "custom",
1055
- mcp_path_kind: endpoint.path === DEFAULT_MCP_PATH ? "default" : "custom"
1056
- };
2063
+ function armWithoutDiscriminant(arm, discriminant) {
2064
+ const properties = { ...arm.properties ?? {} };
2065
+ delete properties[discriminant];
2066
+ const required = (arm.required ?? []).filter((key) => key !== discriminant);
2067
+ const rebuilt = { ...arm, properties, required };
2068
+ if (required.length === 0) delete rebuilt.required;
2069
+ return rebuilt;
1057
2070
  }
1058
- function createLocalMcpHttpHandler(options = {}) {
1059
- const endpoint = resolveLocalMcpEndpoint(options.endpoint);
1060
- const baseHost = endpoint.host === "::1" ? "[::1]" : endpoint.host;
1061
- return async (req, res) => {
1062
- const requestUrl = new URL(req.url ?? "/", `http://${baseHost}:${endpoint.port}`);
1063
- if (requestUrl.pathname !== endpoint.path) {
1064
- jsonRpcError(res, 404, `MCP endpoint is available at ${endpoint.path}`);
1065
- return;
2071
+ function foldArmFindings(listed, discriminant, opId, own) {
2072
+ const findings = [];
2073
+ for (const key of ["oneOf", "anyOf", "allOf"]) {
2074
+ if (listed[key] !== void 0) findings.push(`the listing carries a top-level ${key}`);
2075
+ }
2076
+ if (listed.type !== "object") findings.push("the listing is not type object");
2077
+ const properties = listed.properties ?? {};
2078
+ const selector = properties[discriminant]?.enum;
2079
+ if (!Array.isArray(selector) || !selector.includes(opId)) {
2080
+ findings.push(`${discriminant} does not list ${opId}`);
2081
+ }
2082
+ const ownProperties = own.properties ?? {};
2083
+ for (const [name, field] of Object.entries(ownProperties)) {
2084
+ const shown = properties[name];
2085
+ if (shown === void 0) {
2086
+ findings.push(`${name} is not listed`);
2087
+ continue;
1066
2088
  }
1067
- if (req.method === "OPTIONS") {
1068
- res.writeHead(204, {
1069
- "Access-Control-Allow-Origin": "*",
1070
- "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
1071
- "Access-Control-Allow-Headers": "Content-Type, MCP-Protocol-Version, mcp-session-id"
1072
- });
1073
- res.end();
1074
- return;
2089
+ const { description: ownNote, ...ownShape } = field;
2090
+ const { description: shownNote, ...shownShape } = shown;
2091
+ const merged = Array.isArray(shownShape.anyOf) && Object.keys(shownShape).length === 1;
2092
+ const shapes = merged ? [shownShape, ...shownShape.anyOf] : [shownShape];
2093
+ if (!shapes.some((shape) => JSON.stringify(shape) === JSON.stringify(ownShape))) {
2094
+ findings.push(`${name} is listed with another shape`);
1075
2095
  }
1076
- const server = createAgentQaMcpServer({
1077
- configPath: options.configPath,
1078
- dashboardUrl: options.dashboardUrl,
1079
- endpointUrl: endpoint.url,
1080
- analyticsConfig: options.analyticsConfig,
1081
- transport: "http"
1082
- });
1083
- const transport = new StreamableHTTPServerTransport({
1084
- sessionIdGenerator: void 0
1085
- });
1086
- try {
1087
- await server.connect(transport);
1088
- await transport.handleRequest(req, res);
1089
- res.on("close", () => {
1090
- void transport.close();
1091
- void server.close();
1092
- });
1093
- } catch (err) {
1094
- void transport.close();
1095
- void server.close();
1096
- if (!res.headersSent) {
1097
- jsonRpcError(
1098
- res,
1099
- 500,
1100
- err instanceof Error ? err.message : "Internal MCP server error"
1101
- );
1102
- }
2096
+ const kept = typeof shownNote === "string" && shownNote.includes(String(ownNote));
2097
+ if (typeof ownNote === "string" && !kept) {
2098
+ findings.push(`${name} lost its own description`);
1103
2099
  }
2100
+ }
2101
+ const allowed = /* @__PURE__ */ new Set([discriminant, ...own.required ?? []]);
2102
+ for (const name of listed.required ?? []) {
2103
+ if (!allowed.has(name)) findings.push(`the listing requires ${name}, which ${opId} does not`);
2104
+ }
2105
+ if (own.additionalProperties === false && listed.additionalProperties !== false) {
2106
+ findings.push("the listing admits fields no arm takes");
2107
+ }
2108
+ return findings;
2109
+ }
2110
+ function withoutLegacyResultWrap(document) {
2111
+ const wrapper = document ?? {};
2112
+ const keys = Object.keys(wrapper).filter((key) => key !== "$schema").sort();
2113
+ if (keys.join(",") !== "properties,required,type" || wrapper.type !== "object") return wrapper;
2114
+ const properties = wrapper.properties;
2115
+ const required = wrapper.required;
2116
+ if (Object.keys(properties ?? {}).join(",") !== "result") return wrapper;
2117
+ if (!Array.isArray(required) || required.length !== 1 || required[0] !== "result") return wrapper;
2118
+ const natural = properties.result;
2119
+ if (typeof natural !== "object" || natural === null || natural.type === "object") return wrapper;
2120
+ return natural;
2121
+ }
2122
+
2123
+ // src/task-seam.ts
2124
+ var JOB_STATUS_TO_MCP_TASK_STATE = {
2125
+ // The four checkpoint boundaries. One answer, four times, on purpose.
2126
+ queued: "working",
2127
+ authored: "working",
2128
+ bundling: "working",
2129
+ committing: "working",
2130
+ // The three terminal states, kept apart.
2131
+ done: "completed",
2132
+ failed: "failed",
2133
+ cancelled: "cancelled"
2134
+ };
2135
+ function projectJobAsTask(job) {
2136
+ return {
2137
+ taskId: job.id,
2138
+ status: JOB_STATUS_TO_MCP_TASK_STATE[job.status],
2139
+ statusMessage: job.summary,
2140
+ createdAt: job.createdAt,
2141
+ lastUpdatedAt: job.updatedAt
1104
2142
  };
1105
2143
  }
1106
2144
  export {
1107
- AGENT_QA_SCHEMA_REFERENCES,
1108
- DEFAULT_MCP_HOST,
1109
- DEFAULT_MCP_PATH,
1110
- DEFAULT_MCP_PORT,
1111
- MCP_STDIO_STARTUP_MESSAGE,
1112
- SCHEMA_REFERENCE_NAMES,
1113
- VALIDATION_KINDS,
1114
- classifyRunFailureFromDashboardData,
1115
- createAgentQaMcpServer,
1116
- createLocalMcpHttpHandler,
1117
- createMcpServer,
1118
- resolveDashboardApiUrl,
1119
- resolveLocalMcpEndpoint,
1120
- resolveMcpEndpointShape,
1121
- startMcpServer,
1122
- validateAgentQaDefinition
2145
+ AGENT_QA_COMPOSED_CONTRACTS,
2146
+ AGENT_QA_MCP_INSTRUCTIONS,
2147
+ AGENT_QA_MCP_SERVER_VERSION,
2148
+ AGENT_QA_OP_CONTRACTS,
2149
+ AGENT_QA_TOOL_PROJECTIONS,
2150
+ APP_NEGOTIATION_CLIENT_CAPABILITIES_KEY,
2151
+ APP_RESOURCE_MIME_TYPE,
2152
+ APP_RESOURCE_NAME,
2153
+ APP_RESOURCE_URI_TEMPLATE,
2154
+ APP_WRAPPER_APP_NAME,
2155
+ APP_WRAPPER_APP_VERSION,
2156
+ APP_WRAPPER_CONNECTING_HINT,
2157
+ APP_WRAPPER_CONNECTING_MESSAGE,
2158
+ APP_WRAPPER_EXPIRED_HINT,
2159
+ APP_WRAPPER_EXPIRED_MESSAGE,
2160
+ APP_WRAPPER_LIVE_MESSAGE,
2161
+ APP_WRAPPER_LOAD_TIMEOUT_MS,
2162
+ APP_WRAPPER_PERMANENT_LINK_LABEL,
2163
+ APP_WRAPPER_RUN_ENDED_MESSAGE,
2164
+ APP_WRAPPER_STATES,
2165
+ APP_WRAPPER_UI_PROTOCOL_VERSION,
2166
+ APP_WRAPPER_UNREACHABLE_MESSAGE,
2167
+ AppResourceNegotiationError,
2168
+ AppResourceOriginSpanError,
2169
+ AppResourceReadError,
2170
+ AppResourceUriError,
2171
+ DEGRADED_OUTPUT_CONTRACTS,
2172
+ JOB_STATUS_TO_MCP_TASK_STATE,
2173
+ LIVE_LINK_ABSENT_LINE,
2174
+ LIVE_LINK_NO_RUN_LINE,
2175
+ MCP_APPS_EXTENSION_ID,
2176
+ McpProjectionError,
2177
+ UNPROJECTED_OPS,
2178
+ V1_TOOL_MIGRATION_MAP,
2179
+ advertisedContracts,
2180
+ appResourceUriFor,
2181
+ armWithoutDiscriminant,
2182
+ buildAgentQaMcpServer,
2183
+ compositionOf,
2184
+ createAgentQaMcpNodeHandler,
2185
+ createAppResourceReadHandler,
2186
+ foldArmFindings,
2187
+ liveLinkLine,
2188
+ liveLinkLineFor,
2189
+ liveRunLinkOf,
2190
+ maskedToolResult,
2191
+ negotiatedAppsExtension,
2192
+ parseAppUri,
2193
+ projectJobAsTask,
2194
+ registerAppResources,
2195
+ renderAppWrapperPage,
2196
+ stripRedundantObjectStamp,
2197
+ toRegisterToolConfig,
2198
+ withoutDialect,
2199
+ withoutLegacyResultWrap
1123
2200
  };
1124
2201
  //# sourceMappingURL=index.js.map