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