@sema-agent/server 7.92.1 → 7.93.0
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/README.md +1 -0
- package/README.zh-CN.md +1 -0
- package/dist/approval-ask-machine.d.ts +1 -1
- package/dist/approval-card.d.ts +10 -7
- package/dist/approval-card.js +3 -3
- package/dist/bench/s1/arms.js +5 -6
- package/dist/bench/s1/live-deps.js +2 -2
- package/dist/boot/resolve-spec.js +1 -0
- package/dist/boot/stage-06-runners.js +3 -2
- package/dist/boot/stores.js +9 -0
- package/dist/capabilities/mcp-probe.d.ts +290 -0
- package/dist/capabilities/mcp-probe.js +318 -0
- package/dist/http/resume-legs.js +12 -10
- package/dist/http/route-ctx.d.ts +3 -0
- package/dist/http/routes/capabilities.d.ts +17 -0
- package/dist/http/routes/capabilities.js +93 -1
- package/dist/http/routes/memory-origin.d.ts +1 -1
- package/dist/http/routes/rules.js +49 -13
- package/dist/http/routes/runs.js +3 -2
- package/dist/http/routes/session-sync.js +24 -12
- package/dist/http/routes/sessions.js +12 -8
- package/dist/http/routes/tasks.js +13 -13
- package/dist/http/send.d.ts +11 -0
- package/dist/http/send.js +9 -0
- package/dist/http/server.js +3 -2
- package/dist/http/wire-types.d.ts +17 -1
- package/dist/leader/fanout.js +3 -3
- package/dist/leader/leader.js +4 -4
- package/dist/leader/repair-wire.js +2 -2
- package/dist/leader/wire.js +2 -2
- package/dist/observability/metrics.js +2 -0
- package/dist/permission-rule-vocab.d.ts +9 -9
- package/dist/plugins/file-run-store.js +3 -3
- package/dist/plugins/memory-run-store.js +3 -3
- package/dist/plugins/remote-env-host.d.ts +5 -0
- package/dist/plugins/remote-env-host.js +39 -37
- package/dist/plugins/run-store-sql.js +3 -3
- package/dist/plugins/schema-consistency.d.ts +88 -0
- package/dist/plugins/schema-consistency.js +202 -0
- package/dist/plugins/schema-ensure-legs.d.ts +55 -0
- package/dist/plugins/schema-ensure-legs.js +87 -0
- package/dist/plugins/session-policy-store-sql.js +23 -5
- package/dist/request-key-closure.js +1 -1
- package/dist/rules-consent.d.ts +42 -4
- package/dist/rules-consent.js +23 -7
- package/dist/run-local.js +2 -2
- package/dist/runs.js +14 -14
- package/dist/task-mcp.d.ts +32 -0
- package/dist/task-mcp.js +7 -0
- package/dist/terminal.d.ts +58 -4
- package/dist/terminal.js +28 -6
- package/dist/tool-approval.d.ts +22 -1
- package/dist/tool-approval.js +1 -0
- package/dist/trace/core-keyset-guard.d.ts +3 -3
- package/dist/trace/ledger-sink.js +3 -2
- package/dist/trace/project.d.ts +24 -0
- package/dist/trace/project.js +40 -37
- package/dist/trace/wire-projection-faces.d.ts +209 -0
- package/dist/trace/wire-projection-faces.js +62 -0
- package/package.json +3 -3
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { probeMcpServers } from "@sema-agent/core";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { createFixedWindowRateLimiter } from "../device-enrollment.js";
|
|
4
|
+
import { canonicalJson } from "../canonical-json.js";
|
|
5
|
+
import { echoKeyName, keyClosureClause, scanKeyClosure } from "../request-key-closure.js";
|
|
6
|
+
import { CONTENT_ORIGIN_WORDS, describeBadContentOrigin, isContentOriginWord } from "../mcp-content-origin.js";
|
|
7
|
+
import { HTTP_MCP_TRANSPORT_KEYS, MAX_REQUEST_MCP_SERVERS, MCP_ENTRY_EMPTY_ARRAY_KEYS, MCP_ENTRY_EMPTY_RECORD_KEYS, MCP_SERVER_ENTRY_KEYS, MCP_TRANSPORT_EMPTY_ARRAY_KEYS, MCP_TRANSPORT_EMPTY_RECORD_KEYS, STDIO_MCP_TRANSPORT_KEYS, validateRequestMcp, } from "../task-mcp.js";
|
|
8
|
+
import { buildDisplaySafeUrl } from "../trace/redact.js";
|
|
9
|
+
import { projectWiringManifestMcpRows } from "../trace/project.js";
|
|
10
|
+
export const MCP_PROBE_REFUSAL_CODES = [
|
|
11
|
+
"capability.mcp_injection_required",
|
|
12
|
+
"limit.rate_exceeded",
|
|
13
|
+
"request.body_shape",
|
|
14
|
+
"request.field_invalid",
|
|
15
|
+
"state.mcp_probe_incomplete",
|
|
16
|
+
];
|
|
17
|
+
export const MCP_PROBE_HTTP_STATUS = {
|
|
18
|
+
"capability.mcp_injection_required": 501,
|
|
19
|
+
"limit.rate_exceeded": 429,
|
|
20
|
+
"request.body_shape": 400,
|
|
21
|
+
"request.field_invalid": 400,
|
|
22
|
+
"state.mcp_probe_incomplete": 503,
|
|
23
|
+
};
|
|
24
|
+
export const MCP_PROBE_RATE_POLICY = { limit: 12, windowMs: 60_000 };
|
|
25
|
+
export const MCP_PROBE_CACHE_TTL_MS = 30_000;
|
|
26
|
+
export const MCP_PROBE_CACHE_MAX_ENTRIES = 256;
|
|
27
|
+
export const MCP_PROBE_MAX_INFLIGHT = 32;
|
|
28
|
+
export const MCP_PROBE_VERDICT_TIMEOUT_MS = 10_000;
|
|
29
|
+
export function buildMcpProbeWalkTimeoutMs(declarations) {
|
|
30
|
+
return Math.ceil(Math.max(1, declarations) / MCP_PROBE_CONCURRENCY) * MCP_PROBE_VERDICT_TIMEOUT_MS + MCP_PROBE_RESIDUE_SLACK_MS;
|
|
31
|
+
}
|
|
32
|
+
export const MCP_PROBE_CONCURRENCY = 8;
|
|
33
|
+
export const MCP_PROBE_RESIDUE_SLACK_MS = 4_000;
|
|
34
|
+
export const MCP_PROBE_OUTCOMES = ["probed", "cached", "empty", "gate_closed", "rate_limited", "bad_request", "failed"];
|
|
35
|
+
const PROBE_BODY_KEYS = ["mcpServers"];
|
|
36
|
+
const ACCEPTED_PROBE_BODY = new Set(PROBE_BODY_KEYS);
|
|
37
|
+
const NO_TOLERATED_KEYS = new Set();
|
|
38
|
+
const shapeRefusal = (message, unknownKeys) => ({
|
|
39
|
+
errorCode: "request.body_shape",
|
|
40
|
+
message,
|
|
41
|
+
...(unknownKeys !== undefined ? { unknownKeys } : {}),
|
|
42
|
+
});
|
|
43
|
+
const fieldRefusal = (message) => ({ errorCode: "request.field_invalid", message });
|
|
44
|
+
function entryKeyIssue(entry, at) {
|
|
45
|
+
const buckets = { unknown: [], unsupported: [] };
|
|
46
|
+
scanKeyClosure(entry, `${at}.`, new Set(MCP_SERVER_ENTRY_KEYS), NO_TOLERATED_KEYS, buckets);
|
|
47
|
+
const t = entry.transport;
|
|
48
|
+
if (t !== null && typeof t === "object" && !Array.isArray(t)) {
|
|
49
|
+
const kind = t.kind;
|
|
50
|
+
const keys = kind === "stdio" ? STDIO_MCP_TRANSPORT_KEYS : kind === "http" ? HTTP_MCP_TRANSPORT_KEYS : undefined;
|
|
51
|
+
if (keys !== undefined)
|
|
52
|
+
scanKeyClosure(t, `${at}.transport.`, new Set(keys), NO_TOLERATED_KEYS, buckets);
|
|
53
|
+
}
|
|
54
|
+
if (buckets.unknown.length === 0)
|
|
55
|
+
return null;
|
|
56
|
+
buckets.unknown.sort();
|
|
57
|
+
return shapeRefusal(`body: unrecognized key(s) on an MCP declaration — refused rather than dropped, because a dropped key reads as "in effect" to the caller. ` +
|
|
58
|
+
`${keyClosureClause(buckets.unknown, "unknown (not an mcpServers[] key — check the spelling)")}. ` +
|
|
59
|
+
`Accepted per entry: ${[...MCP_SERVER_ENTRY_KEYS].sort().join(",")} (transport.kind "stdio": ${[...STDIO_MCP_TRANSPORT_KEYS].sort().join(",")}; "http": ${[...HTTP_MCP_TRANSPORT_KEYS].sort().join(",")})`, buckets.unknown);
|
|
60
|
+
}
|
|
61
|
+
function entryValueIssue(entry, at) {
|
|
62
|
+
const read = validateRequestMcp([entry]);
|
|
63
|
+
if (read === null)
|
|
64
|
+
return fieldRefusal(`${at} is not a readable MCP declaration`);
|
|
65
|
+
const dropped = read.dropped[0];
|
|
66
|
+
if (dropped !== undefined || read.ok.length !== 1) {
|
|
67
|
+
const field = dropped?.field ?? "entry";
|
|
68
|
+
return fieldRefusal(`${at}.${field} is not a usable value${urlEchoFor(entry, field)} — this endpoint refuses the whole list rather than dialing a silently shortened one` +
|
|
69
|
+
` (a task submission would instead drop just this declaration and mount its siblings; a probe answering "can I use these" must not quietly answer about fewer)`);
|
|
70
|
+
}
|
|
71
|
+
if (read.badContentOrigin.length > 0) {
|
|
72
|
+
return fieldRefusal(`${at}.contentOrigin must be one of ${CONTENT_ORIGIN_WORDS.map((w) => `"${w}"`).join(" | ")} — got ${describeBadContentOrigin(entry.contentOrigin)} (omit the key for the default: this server's tools classify as external content)`);
|
|
73
|
+
}
|
|
74
|
+
return declarationDowngradeIssue(entry, read.ok[0], at);
|
|
75
|
+
}
|
|
76
|
+
function declarationDowngradeIssue(entry, spec, at) {
|
|
77
|
+
if (spec === undefined)
|
|
78
|
+
return fieldRefusal(`${at} is not a readable MCP declaration`);
|
|
79
|
+
const diff = jsonPathDiff(stripEquivalentEmpties(entry), stripEquivalentEmpties(spec), at);
|
|
80
|
+
if (diff.length === 0)
|
|
81
|
+
return null;
|
|
82
|
+
diff.sort();
|
|
83
|
+
return fieldRefusal(`${keyClosureClause(diff, "unusable value(s)")} — this deployment's declaration reader could not use these exactly as written, and a probe must not dial a configuration that differs from the one you declared` +
|
|
84
|
+
" (a task submission would instead keep the server and silently drop just these, which is why the two faces disagree here on purpose; this endpoint refuses, because the reading would then describe a server you did not ask about)");
|
|
85
|
+
}
|
|
86
|
+
function stripEquivalentEmpties(entry) {
|
|
87
|
+
const emptyRecord = (v) => v !== null && typeof v === "object" && !Array.isArray(v) && Object.keys(v).length === 0;
|
|
88
|
+
const emptyArray = (v) => Array.isArray(v) && v.length === 0;
|
|
89
|
+
const keep = (bag, recordKeys, arrayKeys) => {
|
|
90
|
+
const out = {};
|
|
91
|
+
for (const [k, v] of Object.entries(bag)) {
|
|
92
|
+
if (recordKeys.includes(k) && emptyRecord(v))
|
|
93
|
+
continue;
|
|
94
|
+
if (arrayKeys.includes(k) && emptyArray(v))
|
|
95
|
+
continue;
|
|
96
|
+
out[k] = v;
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
};
|
|
100
|
+
const top = keep(entry, MCP_ENTRY_EMPTY_RECORD_KEYS, MCP_ENTRY_EMPTY_ARRAY_KEYS);
|
|
101
|
+
const t = top.transport;
|
|
102
|
+
if (t !== null && typeof t === "object" && !Array.isArray(t)) {
|
|
103
|
+
top.transport = keep(t, MCP_TRANSPORT_EMPTY_RECORD_KEYS, MCP_TRANSPORT_EMPTY_ARRAY_KEYS);
|
|
104
|
+
}
|
|
105
|
+
return top;
|
|
106
|
+
}
|
|
107
|
+
function jsonPathDiff(a, b, path) {
|
|
108
|
+
if (canonicalJson(a) === canonicalJson(b))
|
|
109
|
+
return [];
|
|
110
|
+
const aRec = a !== null && typeof a === "object" && !Array.isArray(a) ? a : undefined;
|
|
111
|
+
const bRec = b !== null && typeof b === "object" && !Array.isArray(b) ? b : undefined;
|
|
112
|
+
if (aRec !== undefined && bRec !== undefined) {
|
|
113
|
+
const out = [];
|
|
114
|
+
for (const k of new Set([...Object.keys(aRec), ...Object.keys(bRec)]))
|
|
115
|
+
out.push(...jsonPathDiff(aRec[k], bRec[k], `${path}.${k}`));
|
|
116
|
+
return out.length > 0 ? out : [path];
|
|
117
|
+
}
|
|
118
|
+
if (Array.isArray(a) && Array.isArray(b) && a.length === b.length) {
|
|
119
|
+
const out = [];
|
|
120
|
+
for (let i = 0; i < a.length; i++)
|
|
121
|
+
out.push(...jsonPathDiff(a[i], b[i], `${path}[${i}]`));
|
|
122
|
+
return out.length > 0 ? out : [path];
|
|
123
|
+
}
|
|
124
|
+
return [path];
|
|
125
|
+
}
|
|
126
|
+
function urlEchoFor(entry, field) {
|
|
127
|
+
if (field !== "transport.url")
|
|
128
|
+
return "";
|
|
129
|
+
const t = entry.transport;
|
|
130
|
+
const raw = t !== null && typeof t === "object" ? t.url : undefined;
|
|
131
|
+
return typeof raw === "string" ? ` (got "${buildDisplaySafeUrl(raw)}"; an MCP declaration's address must be an http(s) URL — put credentials in transport.headers, never in the URL)` : "";
|
|
132
|
+
}
|
|
133
|
+
export function readMcpProbeBody(raw) {
|
|
134
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
135
|
+
return { ok: false, refusal: shapeRefusal("body must be a JSON object of the shape { mcpServers: [ … ] } (the same declaration list a task submits)") };
|
|
136
|
+
const buckets = { unknown: [], unsupported: [] };
|
|
137
|
+
scanKeyClosure(raw, "", ACCEPTED_PROBE_BODY, NO_TOLERATED_KEYS, buckets);
|
|
138
|
+
if (buckets.unknown.length > 0) {
|
|
139
|
+
buckets.unknown.sort();
|
|
140
|
+
return {
|
|
141
|
+
ok: false,
|
|
142
|
+
refusal: shapeRefusal(`body: unrecognized top-level key(s) — refused rather than dropped, because a dropped key reads as "in effect" to the caller. ` +
|
|
143
|
+
`${keyClosureClause(buckets.unknown, "unknown (this face takes only the declaration list)")}. ` +
|
|
144
|
+
`Accepted: ${[...ACCEPTED_PROBE_BODY].sort().join(",")}`, buckets.unknown),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const list = raw.mcpServers;
|
|
148
|
+
if (!Array.isArray(list))
|
|
149
|
+
return { ok: false, refusal: shapeRefusal("mcpServers must be an array of MCP server declarations (send [] to ask about nothing)") };
|
|
150
|
+
if (list.length > MAX_REQUEST_MCP_SERVERS) {
|
|
151
|
+
return { ok: false, refusal: fieldRefusal(`mcpServers carries ${list.length} declarations, above this deployment's ceiling of ${MAX_REQUEST_MCP_SERVERS} — the probe dials every one of them, so the list is refused whole rather than silently truncated to a partial answer`) };
|
|
152
|
+
}
|
|
153
|
+
for (let i = 0; i < list.length; i++) {
|
|
154
|
+
const at = `mcpServers[${i}]`;
|
|
155
|
+
const entry = list[i];
|
|
156
|
+
if (entry === null || typeof entry !== "object" || Array.isArray(entry))
|
|
157
|
+
return { ok: false, refusal: shapeRefusal(`${at} must be an object`) };
|
|
158
|
+
const keyIssue = entryKeyIssue(entry, at);
|
|
159
|
+
if (keyIssue)
|
|
160
|
+
return { ok: false, refusal: keyIssue };
|
|
161
|
+
const valueIssue = entryValueIssue(entry, at);
|
|
162
|
+
if (valueIssue)
|
|
163
|
+
return { ok: false, refusal: valueIssue };
|
|
164
|
+
}
|
|
165
|
+
const validated = validateRequestMcp(list);
|
|
166
|
+
if (validated === null || validated.ok.length !== list.length) {
|
|
167
|
+
return {
|
|
168
|
+
ok: false,
|
|
169
|
+
refusal: fieldRefusal(`mcpServers: this deployment's declaration reader answered differently for the list (${validated === null ? 0 : validated.ok.length}) than it did entry by entry (${list.length}),` +
|
|
170
|
+
" so the probe refuses rather than dialing a silently shortened list. This is a belt on a path that should not be reachable (the reader is deterministic and the list was already capped)," +
|
|
171
|
+
" so nothing in your body needs changing — if you see it, this deployment has a bug worth reporting"),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
return { ok: true, specs: validated.ok };
|
|
175
|
+
}
|
|
176
|
+
export function buildMcpProbeIdentity(principal) {
|
|
177
|
+
return principal !== undefined && principal.length > 0 ? principal : "local";
|
|
178
|
+
}
|
|
179
|
+
export function buildMcpProbeCacheKey(principal, specs) {
|
|
180
|
+
return `${buildMcpProbeIdentity(principal)}::${createHash("sha256").update(canonicalJson(specs), "utf8").digest("hex")}`;
|
|
181
|
+
}
|
|
182
|
+
export function buildMcpProbeRateKey(principal) {
|
|
183
|
+
return `mcp-probe:${buildMcpProbeIdentity(principal)}`;
|
|
184
|
+
}
|
|
185
|
+
export function createMcpProbeLocal() {
|
|
186
|
+
const rate = createFixedWindowRateLimiter(MCP_PROBE_RATE_POLICY);
|
|
187
|
+
const cache = new Map();
|
|
188
|
+
const pruneCache = (now) => {
|
|
189
|
+
const settled = (v) => v.settledAt !== undefined;
|
|
190
|
+
for (const [k, v] of cache)
|
|
191
|
+
if (v.settledAt !== undefined && now - v.settledAt > MCP_PROBE_CACHE_TTL_MS)
|
|
192
|
+
cache.delete(k);
|
|
193
|
+
if (cache.size <= MCP_PROBE_CACHE_MAX_ENTRIES)
|
|
194
|
+
return;
|
|
195
|
+
const victims = [...cache.entries()].filter(([, v]) => settled(v)).sort((a, b) => (a[1].settledAt ?? 0) - (b[1].settledAt ?? 0));
|
|
196
|
+
for (const [k] of victims) {
|
|
197
|
+
if (cache.size <= MCP_PROBE_CACHE_MAX_ENTRIES)
|
|
198
|
+
break;
|
|
199
|
+
cache.delete(k);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
return {
|
|
203
|
+
check: async (key) => await rate.check(key),
|
|
204
|
+
cached: (key, now) => {
|
|
205
|
+
const hit = cache.get(key);
|
|
206
|
+
if (hit === undefined)
|
|
207
|
+
return undefined;
|
|
208
|
+
if (hit.settledAt === undefined)
|
|
209
|
+
return hit.body;
|
|
210
|
+
if (now - hit.settledAt > MCP_PROBE_CACHE_TTL_MS) {
|
|
211
|
+
cache.delete(key);
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
return hit.body;
|
|
215
|
+
},
|
|
216
|
+
remember: (key, at, walk, now = () => Date.now()) => {
|
|
217
|
+
cache.set(key, { settledAt: undefined, body: walk.answer });
|
|
218
|
+
void walk.dials.then(() => {
|
|
219
|
+
const cur = cache.get(key);
|
|
220
|
+
if (cur?.body === walk.answer)
|
|
221
|
+
cur.settledAt = now();
|
|
222
|
+
}, () => {
|
|
223
|
+
const cur = cache.get(key);
|
|
224
|
+
if (cur?.body === walk.answer)
|
|
225
|
+
cache.delete(key);
|
|
226
|
+
});
|
|
227
|
+
pruneCache(at);
|
|
228
|
+
},
|
|
229
|
+
size: () => cache.size,
|
|
230
|
+
inflight: () => [...cache.values()].filter((v) => v.settledAt === undefined).length,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
export function createMcpProbeWalk(specs, principal, now = Date.now) {
|
|
234
|
+
const bound = buildMcpProbeWalkTimeoutMs(specs.length);
|
|
235
|
+
const controller = new AbortController();
|
|
236
|
+
const dials = probeMcpServers(specs, {
|
|
237
|
+
...(principal !== undefined && principal.length > 0 ? { principal } : {}),
|
|
238
|
+
timeoutMs: MCP_PROBE_VERDICT_TIMEOUT_MS,
|
|
239
|
+
concurrency: MCP_PROBE_CONCURRENCY,
|
|
240
|
+
signal: controller.signal,
|
|
241
|
+
});
|
|
242
|
+
dials.catch(() => undefined);
|
|
243
|
+
let timer;
|
|
244
|
+
const deadline = new Promise((_resolve, reject) => {
|
|
245
|
+
timer = setTimeout(() => {
|
|
246
|
+
controller.abort(new McpProbeIncomplete(bound));
|
|
247
|
+
reject(new McpProbeIncomplete(bound));
|
|
248
|
+
}, bound);
|
|
249
|
+
});
|
|
250
|
+
const answer = (async () => {
|
|
251
|
+
try {
|
|
252
|
+
const result = await Promise.race([dials, deadline]);
|
|
253
|
+
const rows = projectWiringManifestMcpRows(result.entries) ?? [];
|
|
254
|
+
if (rows.length !== specs.length) {
|
|
255
|
+
throw new Error(`mcp probe row count ${rows.length} does not match the ${specs.length} declarations it must align with by index — refusing to publish a list a consumer would mis-pair`);
|
|
256
|
+
}
|
|
257
|
+
return { probedAt: new Date(now()).toISOString(), ttlSec: Math.floor(MCP_PROBE_CACHE_TTL_MS / 1000), servers: rows };
|
|
258
|
+
}
|
|
259
|
+
finally {
|
|
260
|
+
if (timer !== undefined)
|
|
261
|
+
clearTimeout(timer);
|
|
262
|
+
}
|
|
263
|
+
})();
|
|
264
|
+
answer.catch(() => undefined);
|
|
265
|
+
return { answer, dials };
|
|
266
|
+
}
|
|
267
|
+
export class McpProbeIncomplete extends Error {
|
|
268
|
+
boundMs;
|
|
269
|
+
constructor(boundMs) {
|
|
270
|
+
super("mcp probe walk did not settle within this deployment's bound");
|
|
271
|
+
this.boundMs = boundMs;
|
|
272
|
+
this.name = "McpProbeIncomplete";
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
export function buildMcpProbeIncompleteRefusal(boundMs) {
|
|
276
|
+
return {
|
|
277
|
+
errorCode: "state.mcp_probe_incomplete",
|
|
278
|
+
message: `the MCP probe did not finish within ${boundMs / 1000}s, so this endpoint has no honest per-server answer to give and will not return a partial one that reads as complete — ` +
|
|
279
|
+
"the usual cause is a declared server that completes its handshake and then stops answering, which the engine cannot cancel (its dial owns no deadline). " +
|
|
280
|
+
"Retrying may well succeed; if it keeps happening, the declaration that never answers is the thing to fix",
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
export function buildEmptyMcpProbeFace(now = Date.now()) {
|
|
284
|
+
return { probedAt: new Date(now).toISOString(), ttlSec: Math.floor(MCP_PROBE_CACHE_TTL_MS / 1000), servers: [] };
|
|
285
|
+
}
|
|
286
|
+
export function buildMcpInjectionRefusal() {
|
|
287
|
+
return {
|
|
288
|
+
errorCode: "capability.mcp_injection_required",
|
|
289
|
+
message: "this deployment does not honor caller-supplied MCP declarations, so it will not dial them on your behalf: " +
|
|
290
|
+
"either it is multi-tenant (REQUIRE_PRINCIPAL=true — a tenant must not make the shared worker run an arbitrary command or connect to a body-chosen address), " +
|
|
291
|
+
"or `mcp` is an administratively locked config key, or its compliance posture denies `mcp_servers`. " +
|
|
292
|
+
"Read `capabilities.mcpInjection` to tell before asking; `GET /v1/capabilities/mcp` reports this deployment's OWN declared servers and stays available",
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
export function buildMcpProbeRateRefusal() {
|
|
296
|
+
return {
|
|
297
|
+
errorCode: "limit.rate_exceeded",
|
|
298
|
+
message: "MCP probe rate limit exceeded — this face dials every declared server, so it is capped per caller identity " +
|
|
299
|
+
`(${MCP_PROBE_RATE_POLICY.limit} per ${MCP_PROBE_RATE_POLICY.windowMs / 1000}s per replica) independently of every other limit on this deployment; ` +
|
|
300
|
+
`a reading already taken is reused for ${MCP_PROBE_CACHE_TTL_MS / 1000}s and does NOT consume the cap`,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
export function buildMcpProbeInflightRefusal() {
|
|
304
|
+
return {
|
|
305
|
+
errorCode: "limit.rate_exceeded",
|
|
306
|
+
message: `too many MCP probes are still dialing on this replica (${MCP_PROBE_MAX_INFLIGHT} at once) — this endpoint refuses rather than opening one more set of connections, ` +
|
|
307
|
+
"and it deliberately does not drop an in-flight walk to make room (that would remove the one guarantee that keeps a repeated question from re-dialing). " +
|
|
308
|
+
`Retry-After is a HINT, not a promise: each server's verdict is bounded (${MCP_PROBE_VERDICT_TIMEOUT_MS / 1000}s) but a peer that answers the handshake and then stalls holds its dial with no deadline at all`,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
export function buildMcpProbeDeclarationRefusal() {
|
|
312
|
+
return {
|
|
313
|
+
errorCode: "request.field_invalid",
|
|
314
|
+
message: "mcpServers: the engine refused this declaration SET as a whole before dialing anything — the usual cause is two declarations that would mount under one tool-namespace prefix, " +
|
|
315
|
+
"which a task submission refuses the same way, so a probe must not answer as if one of them had been dropped. Give every declared server a name that stays distinct after namespacing",
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
//# sourceMappingURL=mcp-probe.js.map
|
package/dist/http/resume-legs.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { redactErrorMessage, redactHead } from "../observability/run-terminal.js";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import { uuidv7, resumeWithVerification, CheckpointError, defaultTaskRegistry, validatePendingSteer, GLOBAL_USAGE_KEY,
|
|
3
|
+
import { uuidv7, resumeWithVerification, CheckpointError, defaultTaskRegistry, validatePendingSteer, GLOBAL_USAGE_KEY, PAUSE_REGISTRY } from "@sema-agent/core";
|
|
4
4
|
import { decideParkedAgent, denyWorkflowParksUnreadable, findParkOrigin, planWorkflowParkedDecide } from "../parked-decide.js";
|
|
5
5
|
import { HttpError, decodeCheckpointScope, CHECKPOINT_PUBLIC_SCOPE } from "../security.js";
|
|
6
6
|
import { evictIfConflict, parkToolCallId, TurnAnchorCapture, HEARTBEAT_MS, markChildrenStoppedByUserOnAbort } from "../runs.js";
|
|
7
|
-
import { failureCodeOf, nestedPauseOf,
|
|
7
|
+
import { failureCodeOf, nestedPauseOf, sealResultForExit, outwardPlane } from "../terminal.js";
|
|
8
8
|
import { mintCancelledResult } from "../run-cancel-context.js";
|
|
9
9
|
import {} from "../host-decision.js";
|
|
10
10
|
import { readTurnActivityMs, recordTurnActivity } from "../turn-activity.js";
|
|
@@ -30,6 +30,7 @@ import { mintAndDriveDurableRun } from "./durable-run-leg.js";
|
|
|
30
30
|
import { createSessionsLocal } from "./routes/sessions.js";
|
|
31
31
|
import { createSessionSyncLocal } from "./routes/session-sync.js";
|
|
32
32
|
import { createImagesLocal } from "./routes/images.js";
|
|
33
|
+
import { createCapabilitiesLocal } from "./routes/capabilities.js";
|
|
33
34
|
import { gatedPrincipal, explicitOperatorOk } from "./principal-gate.js";
|
|
34
35
|
import { buildActiveRunConflict, resumeEntryForGate } from "./active-run-conflict.js";
|
|
35
36
|
import { commitPlacement, devicePlacementErrorBody } from "./device-placement.js";
|
|
@@ -550,7 +551,7 @@ export function createResumeLegs(ctx) {
|
|
|
550
551
|
resumeLegLive = true;
|
|
551
552
|
const vr = await resumeWithVerification(legRunner, token, outcome, { ...resumeTaskConfig, signal: cancelCtrl.signal, preemptSignal: preemptCtrl.signal }, resumeObjective, verifyRounds, verifyInternals);
|
|
552
553
|
await commitResumeCtx();
|
|
553
|
-
let safe =
|
|
554
|
+
let safe = sealResultForExit(vr);
|
|
554
555
|
const vrNested = nestedPauseOf(vr.terminal);
|
|
555
556
|
const reParkStatus = nestedParkStatusOf(vr);
|
|
556
557
|
const reSuspended = vrNested !== undefined;
|
|
@@ -679,7 +680,7 @@ export function createResumeLegs(ctx) {
|
|
|
679
680
|
if (taskId)
|
|
680
681
|
steerableRuns.set(taskId, stream);
|
|
681
682
|
if (!taskId || !rs)
|
|
682
|
-
return await stream.result();
|
|
683
|
+
return sealResultForExit(await stream.result());
|
|
683
684
|
onAccepted?.(acceptedResumingReceipt(taskId, sessionId));
|
|
684
685
|
for await (const ev of stream) {
|
|
685
686
|
recordTurnActivity(taskId);
|
|
@@ -790,7 +791,7 @@ export function createResumeLegs(ctx) {
|
|
|
790
791
|
}
|
|
791
792
|
await flush();
|
|
792
793
|
const r = await stream.result();
|
|
793
|
-
const rPark = r.terminal.kind === "paused" ?
|
|
794
|
+
const rPark = r.terminal.kind === "paused" ? outwardPlane(r.terminal).status : undefined;
|
|
794
795
|
if (rPark === "suspended") {
|
|
795
796
|
const parkCallId = await parkToolCallId(r, deps.checkpointStore);
|
|
796
797
|
await append("suspended", { gate: r.terminal.kind === "paused" ? r.terminal.gate : null, ...(parkCallId !== undefined ? { toolCallId: parkCallId } : {}) });
|
|
@@ -812,7 +813,7 @@ export function createResumeLegs(ctx) {
|
|
|
812
813
|
}
|
|
813
814
|
return r;
|
|
814
815
|
}
|
|
815
|
-
const safe = await attachModelUsage(
|
|
816
|
+
const safe = await attachModelUsage(sealResultForExit(r), { append, getEvents: (id, a) => rs.getEvents(id, a), modelUsage: deps.modelUsage, taskId });
|
|
816
817
|
await append("done", { result: safe });
|
|
817
818
|
if (taskConfig.suggestNextPrompts && safe.terminal.kind === "completed") {
|
|
818
819
|
const suggestions = await stream.suggestions().catch(() => []);
|
|
@@ -951,16 +952,17 @@ export function createResumeLegs(ctx) {
|
|
|
951
952
|
}
|
|
952
953
|
evictIfConflict(deps.runner, sessionId, result);
|
|
953
954
|
const checkpointReopened = reopenConfirmedPark;
|
|
954
|
-
const
|
|
955
|
+
const sealed = sealResultForExit(result);
|
|
956
|
+
const resumePlane = outwardPlane(sealed.terminal);
|
|
955
957
|
if (taskId && deps.runStore) {
|
|
956
958
|
if (resumePlane.status === "suspended" || checkpointReopened)
|
|
957
959
|
await deps.runStore.setSuspended(taskId);
|
|
958
960
|
else if (resumePlane.status === "needs_review")
|
|
959
961
|
await deps.runStore.setNeedsReview(taskId);
|
|
960
962
|
else
|
|
961
|
-
await deps.runStore.setTerminal(taskId, resumePlane.status,
|
|
963
|
+
await deps.runStore.setTerminal(taskId, resumePlane.status, sealed, resumePlane.errorMessage ?? null);
|
|
962
964
|
}
|
|
963
|
-
settleFleet(checkpointReopened ? "suspended" : resumePlane.status, fleetRunResiduals(
|
|
965
|
+
settleFleet(checkpointReopened ? "suspended" : resumePlane.status, fleetRunResiduals(sealed));
|
|
964
966
|
return {
|
|
965
967
|
status: 200,
|
|
966
968
|
body: {
|
|
@@ -1243,7 +1245,7 @@ export function createResumeLegs(ctx) {
|
|
|
1243
1245
|
deps,
|
|
1244
1246
|
registry: { idemCache, inflightRuns, preemptableRuns, cancelledViaVerb, steerableRuns, wakeParkMints, counters },
|
|
1245
1247
|
helpers: { readJson, readRawBody, rateLimited, quotaExceeded, ownerQuotaDenied, leaseDenied, ownerLeaseDenied, usageWindowDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk },
|
|
1246
|
-
local: { images: createImagesLocal(deps), sessions: createSessionsLocal(), sessionSync: createSessionSyncLocal() },
|
|
1248
|
+
local: { images: createImagesLocal(deps), sessions: createSessionsLocal(), sessionSync: createSessionSyncLocal(), capabilities: createCapabilitiesLocal() },
|
|
1247
1249
|
legs: { prepareSpec, finalizeTaskResult, resumeCheckpoint, driveResumeIntoRunLog, resumePreempted, resumeWake, resumePlanReview },
|
|
1248
1250
|
};
|
|
1249
1251
|
return { routeCtxBase, resumeCheckpoint, driveResumeIntoRunLog, driveResumeLeg, mayDiscloseRowError, resumePreempted, startFreshHostTurn, resumeWake, resumePlanReview };
|
package/dist/http/route-ctx.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ import type { FlatServiceDeps, RequestAuth } from "./server.js";
|
|
|
21
21
|
import type { TaskRequestBody, DecideBinding } from "./wire-types.js";
|
|
22
22
|
import type { IdempotencyCache, SubmitOutcome } from "./idempotency.js";
|
|
23
23
|
import type { ImagesLocal } from "./routes/images.js";
|
|
24
|
+
import type { CapabilitiesLocal } from "./routes/capabilities.js";
|
|
24
25
|
import type { SessionsLocal } from "./routes/sessions.js";
|
|
25
26
|
import type { SessionSyncLocal } from "./routes/session-sync.js";
|
|
26
27
|
import type { PlanApprovePermissionModeAfter } from "../task-settings.js";
|
|
@@ -87,6 +88,8 @@ export interface RouteHelpers {
|
|
|
87
88
|
* (模块级会把 rate-limiter / 缓存跨实例串味,测试里一个进程建几十个 server)。 */
|
|
88
89
|
export interface RouteLocals {
|
|
89
90
|
images: ImagesLocal;
|
|
91
|
+
/** S-481:`GET /v1/capabilities/mcp` / `POST /v1/capabilities/mcp/probe` 的 TTL 缓存与每 principal 限速窗。 */
|
|
92
|
+
capabilities: CapabilitiesLocal;
|
|
90
93
|
sessions: SessionsLocal;
|
|
91
94
|
sessionSync: SessionSyncLocal;
|
|
92
95
|
}
|
|
@@ -6,10 +6,19 @@
|
|
|
6
6
|
* 本域零可变状态;deps 面宽(能力布尔逐条对应各路由的 501 门,单源不许漂移)但全是存在性只读。
|
|
7
7
|
*/
|
|
8
8
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
9
|
+
import { type McpProbeLocal } from "../../capabilities/mcp-probe.js";
|
|
9
10
|
import type { RouteCtx, RouteMatch, RouteIdsOf } from "../route-ctx.js";
|
|
10
11
|
/** 路由体 = 从 `server.ts` 的 `handle()` 里**整段剪切**过来的原文(唯一改动:统一去缩进两格)。裸 `return;`
|
|
11
12
|
* 的语义原样 =「本域已应答」;走到函数尾才是「没匹配上」,置 `miss.fell` 交回装配器继续下一个域。
|
|
12
13
|
* 跨域的东西(deps / 助手 / 单域状态 / 每请求量)一律在首行解构出来,于是下面每一行都逐字不变。 */
|
|
14
|
+
/**
|
|
15
|
+
* S-481 —— 本域独占的运行期状态(每 `createHttpServer` 实例一份)。**模块级不行**:测试里一个进程建几十台
|
|
16
|
+
* server,一张模块级缓存会把 A 台的探测读数发给 B 台,一只模块级限速窗会让第二台起来就被第一台的计数挡住。
|
|
17
|
+
*/
|
|
18
|
+
export interface CapabilitiesLocal {
|
|
19
|
+
readonly mcpProbe: McpProbeLocal;
|
|
20
|
+
}
|
|
21
|
+
export declare function createCapabilitiesLocal(): CapabilitiesLocal;
|
|
13
22
|
export declare function handleCapabilities(req: IncomingMessage, res: ServerResponse, match: RouteMatch<CapabilitiesRouteId>, ctx: RouteCtx): Promise<void>;
|
|
14
23
|
export declare const CAPABILITIES_ROUTES: readonly [{
|
|
15
24
|
readonly id: "models";
|
|
@@ -24,6 +33,14 @@ export declare const CAPABILITIES_ROUTES: readonly [{
|
|
|
24
33
|
readonly pattern: RegExp;
|
|
25
34
|
readonly label: "/v1/capabilities/scenarios/:name";
|
|
26
35
|
readonly methods: readonly ["GET"];
|
|
36
|
+
}, {
|
|
37
|
+
readonly id: "capability-mcp";
|
|
38
|
+
readonly path: "/v1/capabilities/mcp";
|
|
39
|
+
readonly methods: readonly ["GET"];
|
|
40
|
+
}, {
|
|
41
|
+
readonly id: "capability-mcp-probe";
|
|
42
|
+
readonly path: "/v1/capabilities/mcp/probe";
|
|
43
|
+
readonly methods: readonly ["POST"];
|
|
27
44
|
}];
|
|
28
45
|
/** 本域可分派行的 `id` 闭集 —— handler 的 `switch` 按它判穷尽(漏一口 = 编译红)。 */
|
|
29
46
|
export type CapabilitiesRouteId = RouteIdsOf<typeof CAPABILITIES_ROUTES>;
|
|
@@ -6,6 +6,8 @@ import { cwdHonored, deviceCwdHonored } from "../../task-cwd.js";
|
|
|
6
6
|
import { DEVICE_PROTOCOL_VERSION, DEVICE_WS_PATH } from "../../device-ws-protocol.js";
|
|
7
7
|
import { a2aInjectionHonored } from "../../task-a2a.js";
|
|
8
8
|
import { mcpInjectionHonored } from "../../task-mcp.js";
|
|
9
|
+
import { buildEmptyMcpProbeFace, buildMcpInjectionRefusal, buildMcpProbeCacheKey, buildMcpProbeInflightRefusal, buildMcpProbeRateKey, buildMcpProbeRateRefusal, createMcpProbeLocal, buildMcpProbeDeclarationRefusal, buildMcpProbeIncompleteRefusal, McpProbeIncomplete, MCP_PROBE_HTTP_STATUS, MCP_PROBE_MAX_INFLIGHT, MCP_PROBE_VERDICT_TIMEOUT_MS, createMcpProbeWalk, readMcpProbeBody, } from "../../capabilities/mcp-probe.js";
|
|
10
|
+
import { mcpForScenario } from "../../config-center/facade.js";
|
|
9
11
|
import { sendJson, sendError } from "../send.js";
|
|
10
12
|
import { assertNever } from "../route-ctx.js";
|
|
11
13
|
import { resolveStreamApprovalGate } from "../../tool-approval.js";
|
|
@@ -19,7 +21,7 @@ import { workflowModelAllowlistFor, selfOrchestrationDenial } from "../../task-w
|
|
|
19
21
|
import { centerEntitlementSourceWired } from "../../runtime-caps-resolver.js";
|
|
20
22
|
import { judgeAutoModeArming } from "../../auto-mode-face.js";
|
|
21
23
|
import { coercePermissionModeStrict, SETTINGS_PERMISSION_MODE_WORDS } from "../../task-settings.js";
|
|
22
|
-
import { gatedPrincipal } from "../principal-gate.js";
|
|
24
|
+
import { gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
|
|
23
25
|
function routePairingOf(deps, name, m) {
|
|
24
26
|
if (isSealedKeyPoison(deps.config.modelApiKeys?.[name]))
|
|
25
27
|
return "unknown";
|
|
@@ -33,6 +35,9 @@ function retentionAdvertisement(config) {
|
|
|
33
35
|
return null;
|
|
34
36
|
return { mode: sweep.mode, maxAgeDays: policy.maxAgeDays };
|
|
35
37
|
}
|
|
38
|
+
export function createCapabilitiesLocal() {
|
|
39
|
+
return { mcpProbe: createMcpProbeLocal() };
|
|
40
|
+
}
|
|
36
41
|
export async function handleCapabilities(req, res, match, ctx) {
|
|
37
42
|
const { deps } = ctx;
|
|
38
43
|
switch (match.decl.id) {
|
|
@@ -138,6 +143,7 @@ export async function handleCapabilities(req, res, match, ctx) {
|
|
|
138
143
|
modelUsage: Boolean(deps.runStore && deps.modelUsage),
|
|
139
144
|
sessionInit: true,
|
|
140
145
|
mcp: true,
|
|
146
|
+
mcpProbe: true,
|
|
141
147
|
mcpElicitation: Boolean(deps.elicitation),
|
|
142
148
|
askUserQuestion: Boolean(deps.question),
|
|
143
149
|
toolApproval: Boolean(deps.toolApproval),
|
|
@@ -188,6 +194,33 @@ export async function handleCapabilities(req, res, match, ctx) {
|
|
|
188
194
|
});
|
|
189
195
|
return;
|
|
190
196
|
}
|
|
197
|
+
case "capability-mcp": {
|
|
198
|
+
const principal = gatedPrincipal(req, deps.config);
|
|
199
|
+
if (deps.config.requirePrincipal && !principal) {
|
|
200
|
+
sendPrincipalRefusal(res, req, deps.config);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
await serveMcpProbeFace(res, ctx, principal, mcpForScenario(deps.config.mcpServers, deps.config.defaultScenario) ?? [], "deployment");
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
case "capability-mcp-probe": {
|
|
207
|
+
const principal = gatedPrincipal(req, deps.config);
|
|
208
|
+
if (deps.config.requirePrincipal && !principal) {
|
|
209
|
+
sendPrincipalRefusal(res, req, deps.config);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (!mcpInjectionHonored(deps.config)) {
|
|
213
|
+
sendMcpProbeRefusal(res, ctx, buildMcpInjectionRefusal(), "gate_closed");
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const parsed = readMcpProbeBody(await ctx.helpers.readJson(req));
|
|
217
|
+
if (!parsed.ok) {
|
|
218
|
+
sendMcpProbeRefusal(res, ctx, parsed.refusal, "bad_request");
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
await serveMcpProbeFace(res, ctx, principal, parsed.specs, "caller");
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
191
224
|
case "models": {
|
|
192
225
|
const atCatalog = expandTiers(deps.config.models, deps.config.tiers ?? {}) ?? deps.config.models;
|
|
193
226
|
const atAllowlist = deps.config.atModelAllowlist ?? [];
|
|
@@ -215,9 +248,68 @@ export async function handleCapabilities(req, res, match, ctx) {
|
|
|
215
248
|
return assertNever(match.decl.id);
|
|
216
249
|
}
|
|
217
250
|
}
|
|
251
|
+
function sendMcpProbeRefusal(res, ctx, refusal, outcome, retryAfterSec) {
|
|
252
|
+
ctx.deps.metrics?.inc("mcp_probe_total", { outcome });
|
|
253
|
+
const status = MCP_PROBE_HTTP_STATUS[refusal.errorCode];
|
|
254
|
+
if (retryAfterSec !== undefined)
|
|
255
|
+
res.setHeader("retry-after", String(retryAfterSec));
|
|
256
|
+
sendError(res, status, refusal.errorCode, refusal.message, {
|
|
257
|
+
...(refusal.unknownKeys !== undefined ? { unknownKeys: refusal.unknownKeys } : {}),
|
|
258
|
+
...(retryAfterSec !== undefined ? { retryAfterSec } : {}),
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
async function serveMcpProbeFace(res, ctx, principal, specs, declaredBy) {
|
|
262
|
+
const { deps } = ctx;
|
|
263
|
+
const local = ctx.local.capabilities.mcpProbe;
|
|
264
|
+
if (specs.length === 0) {
|
|
265
|
+
deps.metrics?.inc("mcp_probe_total", { outcome: "empty" });
|
|
266
|
+
sendJson(res, 200, buildEmptyMcpProbeFace());
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const key = buildMcpProbeCacheKey(principal, specs);
|
|
270
|
+
const hit = local.cached(key, Date.now());
|
|
271
|
+
if (hit !== undefined) {
|
|
272
|
+
await answerMcpProbe(res, ctx, hit, "cached", declaredBy);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (local.inflight() >= MCP_PROBE_MAX_INFLIGHT) {
|
|
276
|
+
sendMcpProbeRefusal(res, ctx, buildMcpProbeInflightRefusal(), "rate_limited", Math.ceil(MCP_PROBE_VERDICT_TIMEOUT_MS / 1000));
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const verdict = await local.check(buildMcpProbeRateKey(principal));
|
|
280
|
+
if (!verdict.allowed) {
|
|
281
|
+
sendMcpProbeRefusal(res, ctx, buildMcpProbeRateRefusal(), "rate_limited", verdict.retryAfterSec);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const walk = createMcpProbeWalk(specs, principal);
|
|
285
|
+
local.remember(key, Date.now(), walk);
|
|
286
|
+
await answerMcpProbe(res, ctx, walk.answer, "probed", declaredBy);
|
|
287
|
+
}
|
|
288
|
+
async function answerMcpProbe(res, ctx, answer, outcome, declaredBy) {
|
|
289
|
+
const { deps } = ctx;
|
|
290
|
+
try {
|
|
291
|
+
const body = await answer;
|
|
292
|
+
sendJson(res, 200, body);
|
|
293
|
+
deps.metrics?.inc("mcp_probe_total", { outcome });
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
deps.metrics?.inc("mcp_probe_total", { outcome: "failed" });
|
|
297
|
+
if (err instanceof McpProbeIncomplete) {
|
|
298
|
+
sendMcpProbeRefusal(res, ctx, buildMcpProbeIncompleteRefusal(err.boundMs), "failed", Math.ceil(err.boundMs / 1000));
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (declaredBy === "caller") {
|
|
302
|
+
sendMcpProbeRefusal(res, ctx, buildMcpProbeDeclarationRefusal(), "failed");
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
throw err;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
218
308
|
export const CAPABILITIES_ROUTES = [
|
|
219
309
|
{ id: "models", path: "/v1/models", methods: ["GET"] },
|
|
220
310
|
{ id: "capabilities", path: "/v1/capabilities", methods: ["GET"] },
|
|
221
311
|
{ id: "capability-scenario", pattern: /^\/v1\/capabilities\/scenarios\/(?<name>.*)$/, label: "/v1/capabilities/scenarios/:name", methods: ["GET"] },
|
|
312
|
+
{ id: "capability-mcp", path: "/v1/capabilities/mcp", methods: ["GET"] },
|
|
313
|
+
{ id: "capability-mcp-probe", path: "/v1/capabilities/mcp/probe", methods: ["POST"] },
|
|
222
314
|
];
|
|
223
315
|
//# sourceMappingURL=capabilities.js.map
|
|
@@ -105,7 +105,7 @@ export declare function buildClearanceRowWire(r: OriginClearanceRow): {
|
|
|
105
105
|
requestId: string;
|
|
106
106
|
reason: string;
|
|
107
107
|
at: number;
|
|
108
|
-
status: "
|
|
108
|
+
status: "pending" | "failed" | "done";
|
|
109
109
|
originUnknownKeys?: string[] | undefined;
|
|
110
110
|
clearanceId: string;
|
|
111
111
|
entryId: string;
|