@sema-agent/core 5.54.0 → 5.56.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/CHANGELOG.md +160 -0
- package/dist/agents/cumulative-stats.d.ts +26 -0
- package/dist/agents/cumulative-stats.js +56 -0
- package/dist/agents/observer.d.ts +11 -7
- package/dist/agents/observer.js +2 -4
- package/dist/agents/send-message-tool.js +48 -2
- package/dist/agents/subagent.js +250 -89
- package/dist/agents/verify.d.ts +27 -3
- package/dist/agents/verify.js +7 -2
- package/dist/core/auto-compaction.d.ts +17 -4
- package/dist/core/auto-compaction.js +3 -0
- package/dist/core/context-edit.d.ts +55 -6
- package/dist/core/context-edit.js +12 -1
- package/dist/core/governance-codes.js +14 -0
- package/dist/core/hooks.d.ts +293 -11
- package/dist/core/hooks.js +159 -12
- package/dist/core/human-input-projection.d.ts +20 -2
- package/dist/core/human-input-projection.js +9 -0
- package/dist/core/lsp-diagnostics.d.ts +19 -17
- package/dist/core/lsp-diagnostics.js +11 -5
- package/dist/core/mcp.d.ts +46 -0
- package/dist/core/mcp.js +132 -6
- package/dist/core/memory-engine/consolidation.d.ts +378 -0
- package/dist/core/memory-engine/consolidation.js +342 -0
- package/dist/core/memory-engine/dual-root.js +3 -0
- package/dist/core/memory-engine/engine.d.ts +237 -4
- package/dist/core/memory-engine/engine.js +1111 -4
- package/dist/core/memory-engine/export-bundle.js +9 -0
- package/dist/core/memory-engine/file-backend.js +27 -1
- package/dist/core/memory-engine/frontmatter.d.ts +20 -1
- package/dist/core/memory-engine/frontmatter.js +111 -0
- package/dist/core/memory-engine/index.d.ts +4 -2
- package/dist/core/memory-engine/index.js +3 -1
- package/dist/core/memory-engine/memory-backend-contract.js +131 -0
- package/dist/core/memory-engine/sync-client.js +26 -0
- package/dist/core/memory-engine/tools.d.ts +9 -0
- package/dist/core/memory-engine/tools.js +57 -13
- package/dist/core/memory-engine/types.d.ts +99 -0
- package/dist/core/memory-recall.js +4 -3
- package/dist/core/memory.d.ts +33 -3
- package/dist/core/memory.js +6 -4
- package/dist/core/permission-rules.d.ts +30 -0
- package/dist/core/permission-rules.js +71 -8
- package/dist/core/reminder-disclosure.d.ts +29 -4
- package/dist/core/reminder-disclosure.js +60 -12
- package/dist/core/runner/prepare-memory.js +7 -2
- package/dist/core/runner/prepare-task.d.ts +39 -1
- package/dist/core/runner/prepare-task.js +63 -35
- package/dist/core/runner/runtask.d.ts +8 -1
- package/dist/core/runner/runtask.js +170 -31
- package/dist/core/runner/session-rule-policy.js +5 -3
- package/dist/core/runner/synthetic-tools.js +4 -2
- package/dist/core/runner/turn-attachments.d.ts +16 -6
- package/dist/core/runner/turn-attachments.js +34 -20
- package/dist/core/session-reconcile.d.ts +32 -0
- package/dist/core/session-reconcile.js +15 -0
- package/dist/core/task-notification.d.ts +34 -7
- package/dist/core/task-notification.js +11 -1
- package/dist/core/task-registry-agent.d.ts +20 -3
- package/dist/core/task-registry-agent.js +31 -2
- package/dist/core/tool-policy.d.ts +23 -0
- package/dist/core/tool-policy.js +29 -13
- package/dist/core/types.d.ts +126 -17
- package/dist/core/untrusted-egress.js +12 -2
- package/dist/core/untrusted-text.d.ts +189 -3
- package/dist/core/untrusted-text.js +424 -6
- package/dist/engine/compaction/compaction.d.ts +77 -7
- package/dist/engine/compaction/compaction.js +98 -9
- package/dist/engine/compaction/utils.d.ts +4 -0
- package/dist/engine/compaction/utils.js +6 -0
- package/dist/engine/harness/agent-harness.d.ts +84 -0
- package/dist/engine/harness/agent-harness.js +88 -12
- package/dist/engine/harness/messages.d.ts +4 -2
- package/dist/engine/harness/messages.js +7 -2
- package/dist/engine/harness/types.d.ts +11 -5
- package/dist/engine/loop/types.d.ts +14 -0
- package/dist/engine/session/import-validate.js +10 -0
- package/dist/engine/session/session.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/orchestration/run-spec.js +8 -1
- package/dist/prompts/default.d.ts +22 -6
- package/dist/tools/fs/index.d.ts +3 -1
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +28 -1
package/dist/core/hooks.js
CHANGED
|
@@ -6,6 +6,92 @@ import { PROBE_REASON_MAX, normalizeProbeCause } from "./checkpoint-store.js";
|
|
|
6
6
|
import { ASK_USER_QUESTION_TOOL_NAME } from "./ask-question.js";
|
|
7
7
|
import { createSafeNotifier } from "./safe-notify.js";
|
|
8
8
|
import { ORG_ADJUDICATION_TIMEOUT_MS, ORG_RULE_DECISION_REASON, ORG_UNAVAILABLE_DECISION_REASON, settleOrgVerdictWithin } from "./permission-rule-org.js";
|
|
9
|
+
export const DEFAULT_HOOK_TIMEOUT_MS = 600_000;
|
|
10
|
+
const MAX_HOOK_TIMEOUT_MS = 2_147_483_647;
|
|
11
|
+
const POST_ABORT_GRACE_MS = 1_000;
|
|
12
|
+
export function postAbortGraceMs(remainingMs) {
|
|
13
|
+
return Math.max(0, Math.min(POST_ABORT_GRACE_MS, remainingMs));
|
|
14
|
+
}
|
|
15
|
+
const badHookTimeoutReported = new WeakSet();
|
|
16
|
+
export function resolveHookTimeoutMs(supplied, report, owner) {
|
|
17
|
+
if (supplied === undefined)
|
|
18
|
+
return DEFAULT_HOOK_TIMEOUT_MS;
|
|
19
|
+
if (Number.isFinite(supplied) && supplied >= 0 && supplied <= MAX_HOOK_TIMEOUT_MS)
|
|
20
|
+
return supplied;
|
|
21
|
+
if (report !== undefined && !(owner !== undefined && badHookTimeoutReported.has(owner))) {
|
|
22
|
+
if (owner !== undefined)
|
|
23
|
+
badHookTimeoutReported.add(owner);
|
|
24
|
+
try {
|
|
25
|
+
report(new Error(`Hooks.timeoutMs must be a non-negative finite number no greater than ${MAX_HOOK_TIMEOUT_MS} (got ${String(supplied)}) — ` +
|
|
26
|
+
`every hook seat falls back to the ${DEFAULT_HOOK_TIMEOUT_MS}ms default`));
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return DEFAULT_HOOK_TIMEOUT_MS;
|
|
32
|
+
}
|
|
33
|
+
export async function runHookSeat(seat, bound, call) {
|
|
34
|
+
void seat;
|
|
35
|
+
const timeoutMs = resolveHookTimeoutMs(bound.timeoutMs, bound.onBadTimeout, bound.owner);
|
|
36
|
+
const taskSignal = bound.signal;
|
|
37
|
+
const controller = new AbortController();
|
|
38
|
+
const pending = Promise.resolve(call(controller.signal));
|
|
39
|
+
const startedAt = performance.now();
|
|
40
|
+
return await new Promise((resolve, reject) => {
|
|
41
|
+
let settled = false;
|
|
42
|
+
let listening = false;
|
|
43
|
+
let timer;
|
|
44
|
+
const clearTimer = () => {
|
|
45
|
+
if (timer !== undefined)
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
timer = undefined;
|
|
48
|
+
};
|
|
49
|
+
const finish = (act) => {
|
|
50
|
+
if (settled)
|
|
51
|
+
return;
|
|
52
|
+
settled = true;
|
|
53
|
+
clearTimer();
|
|
54
|
+
if (listening)
|
|
55
|
+
taskSignal?.removeEventListener("abort", onAbort);
|
|
56
|
+
act();
|
|
57
|
+
};
|
|
58
|
+
const expire = (cause) => finish(() => {
|
|
59
|
+
void pending.then(undefined, () => {
|
|
60
|
+
});
|
|
61
|
+
resolve({ expired: true, cause });
|
|
62
|
+
queueMicrotask(() => {
|
|
63
|
+
try {
|
|
64
|
+
controller.abort();
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
const collapseToGrace = () => {
|
|
71
|
+
if (settled)
|
|
72
|
+
return;
|
|
73
|
+
clearTimer();
|
|
74
|
+
timer = setTimeout(() => expire("aborted"), postAbortGraceMs(timeoutMs - (performance.now() - startedAt)));
|
|
75
|
+
};
|
|
76
|
+
const onAbort = () => (bound.abortEnds === true ? expire("aborted") : collapseToGrace());
|
|
77
|
+
timer = setTimeout(() => expire("timeout"), timeoutMs);
|
|
78
|
+
if (taskSignal !== undefined) {
|
|
79
|
+
if (taskSignal.aborted) {
|
|
80
|
+
onAbort();
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
taskSignal.addEventListener("abort", onAbort, { once: true });
|
|
84
|
+
listening = true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
pending.then((value) => finish(() => resolve({ expired: false, value })), (err) => finish(() => reject(err instanceof Error ? err : new Error(String(err)))));
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
export function hookSeatExpiredError(seat, timeoutMs, cause, consequence) {
|
|
91
|
+
return new Error(cause === "timeout"
|
|
92
|
+
? `the deployment's ${seat} hook did not answer within its ${timeoutMs}ms bound (Hooks.timeoutMs) — ${consequence}`
|
|
93
|
+
: `the deployment's ${seat} hook had not answered when the task was cancelled — ${consequence}`);
|
|
94
|
+
}
|
|
9
95
|
export function cloneObserverInput(input) {
|
|
10
96
|
try {
|
|
11
97
|
return structuredClone(input);
|
|
@@ -202,17 +288,35 @@ function preToolUseCrashReason(subject, err) {
|
|
|
202
288
|
`This is a failure of the deployment's hook, NOT of the tool itself — an identical retry reaches the ` +
|
|
203
289
|
`same hook and fails the same way. Hook error: ${cause || "(no message)"}`);
|
|
204
290
|
}
|
|
291
|
+
function preToolUseUnansweredReason(subject, timeoutMs, cause) {
|
|
292
|
+
return cause === "timeout"
|
|
293
|
+
? `a PreToolUse hook did not answer within its ${timeoutMs}ms bound while screening ${subject}; the call was ` +
|
|
294
|
+
`NOT executed (fail-closed). This is a failure of the deployment's hook, NOT of the tool itself — the ` +
|
|
295
|
+
`screening face never returned a verdict, so nothing was decided about this call either way.`
|
|
296
|
+
: `the task was cancelled while a PreToolUse hook was still screening ${subject}; the call was NOT executed ` +
|
|
297
|
+
`(fail-closed). No verdict was reached — this is the run ending, not the tool failing.`;
|
|
298
|
+
}
|
|
205
299
|
function screenPreToolUseResult(r) {
|
|
206
300
|
if (r === undefined)
|
|
207
301
|
return undefined;
|
|
208
302
|
return refuseOutOfContractDecision(r, { reasonIsNonInput: true });
|
|
209
303
|
}
|
|
210
|
-
export function createPreToolUseConstraintPolicy(preToolUse, env, onCrash) {
|
|
304
|
+
export function createPreToolUseConstraintPolicy(preToolUse, env, onCrash, timeoutMs) {
|
|
211
305
|
return brandPolicyAskClass({
|
|
212
306
|
check: async (req) => {
|
|
213
307
|
let r;
|
|
308
|
+
const boundMs = resolveHookTimeoutMs(timeoutMs, onCrash);
|
|
214
309
|
try {
|
|
215
|
-
|
|
310
|
+
const seat = await runHookSeat("preToolUse", { timeoutMs: boundMs }, (sig) => preToolUse(req.toolName, req.args, { toolCallId: req.toolCallId, toolName: req.toolName, signal: sig, ...(env !== undefined ? { env } : {}) }));
|
|
311
|
+
if (seat.expired) {
|
|
312
|
+
try {
|
|
313
|
+
onCrash?.(hookSeatExpiredError("preToolUse", boundMs, seat.cause, `the inherited screening of "${req.toolName}" was refused fail-closed`));
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
}
|
|
317
|
+
return { action: "deny", message: preToolUseUnansweredReason(`this call to "${req.toolName}"`, boundMs, seat.cause), decisionReason: "hook" };
|
|
318
|
+
}
|
|
319
|
+
r = screenPreToolUseResult(seat.value);
|
|
216
320
|
}
|
|
217
321
|
catch (err) {
|
|
218
322
|
try {
|
|
@@ -284,9 +388,14 @@ export function persistedRuleMandateOf(marks) {
|
|
|
284
388
|
export async function runToolGate(input) {
|
|
285
389
|
const { event, preToolUse, adjudicate, resolveAsk, suspendAsk } = input;
|
|
286
390
|
const { toolCallId, toolName } = event;
|
|
287
|
-
const hookCtx = () => ({
|
|
391
|
+
const hookCtx = (seatSignal) => ({
|
|
288
392
|
toolCallId,
|
|
289
393
|
toolName,
|
|
394
|
+
...(seatSignal !== undefined ? { signal: seatSignal } : {}),
|
|
395
|
+
...(() => {
|
|
396
|
+
const c = input.trackedCwd?.();
|
|
397
|
+
return typeof c === "string" ? { cwd: c } : {};
|
|
398
|
+
})(),
|
|
290
399
|
...(input.hookEnv !== undefined ? { env: input.hookEnv } : {}),
|
|
291
400
|
...(input.identity !== undefined ? { identity: input.identity } : {}),
|
|
292
401
|
});
|
|
@@ -296,17 +405,47 @@ export async function runToolGate(input) {
|
|
|
296
405
|
let parkFailed;
|
|
297
406
|
let askDenyResolution;
|
|
298
407
|
const notifier = createSafeNotifier(input.onNotifyError !== undefined ? { onError: input.onNotifyError } : undefined);
|
|
408
|
+
const hookSeatMs = resolveHookTimeoutMs(input.hookTimeoutMs, (err) => traceHookCrash(input, err, notifier));
|
|
409
|
+
const seatBound = (abortEnds) => ({
|
|
410
|
+
timeoutMs: hookSeatMs,
|
|
411
|
+
...(input.abortSignal !== undefined ? { signal: input.abortSignal } : {}),
|
|
412
|
+
...(abortEnds ? { abortEnds: true } : {}),
|
|
413
|
+
});
|
|
414
|
+
const preToolUseUnansweredBlock = async (subject, cause) => {
|
|
415
|
+
const reason = preToolUseUnansweredReason(subject, hookSeatMs, cause);
|
|
416
|
+
if (cause === "timeout") {
|
|
417
|
+
traceHookCrash(input, hookSeatExpiredError("preToolUse", hookSeatMs, cause, `${subject} was NOT executed (fail-closed) and the screening face was signalled to stop`), notifier);
|
|
418
|
+
}
|
|
419
|
+
return reason;
|
|
420
|
+
};
|
|
421
|
+
const notifyPermissionDeniedSeat = async (payload) => {
|
|
422
|
+
if (input.permissionDenied === undefined)
|
|
423
|
+
return;
|
|
424
|
+
await notifier.notifyAsync(async () => {
|
|
425
|
+
const seat = await runHookSeat("permissionDenied", seatBound(false), (sig) => input.permissionDenied?.({ ...payload, signal: sig }));
|
|
426
|
+
if (seat.expired)
|
|
427
|
+
throw hookSeatExpiredError("permissionDenied", hookSeatMs, seat.cause, "the deny observation was abandoned; the deny itself is unchanged");
|
|
428
|
+
}, "toolGate.permissionDenied");
|
|
429
|
+
};
|
|
299
430
|
if (preToolUse) {
|
|
300
431
|
let r;
|
|
432
|
+
let unanswered;
|
|
301
433
|
try {
|
|
302
|
-
|
|
434
|
+
const seat = await runHookSeat("preToolUse", seatBound(true), (sig) => preToolUse(toolName, currentInput, hookCtx(sig)));
|
|
435
|
+
if (seat.expired)
|
|
436
|
+
unanswered = seat.cause;
|
|
437
|
+
else
|
|
438
|
+
r = screenPreToolUseResult(seat.value);
|
|
303
439
|
}
|
|
304
440
|
catch (err) {
|
|
305
441
|
const reason = preToolUseCrashReason(`this call to "${toolName}"`, err);
|
|
306
442
|
traceHookCrash(input, err, notifier);
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
443
|
+
await notifyPermissionDeniedSeat({ toolName, input: cloneObserverInput(currentInput), toolCallId, reason, source: "hook", ...(input.identity !== undefined ? { identity: input.identity } : {}) });
|
|
444
|
+
return { block: true, reason: formatHookFeedback(reason, input.reminderMark), preToolContext };
|
|
445
|
+
}
|
|
446
|
+
if (unanswered !== undefined) {
|
|
447
|
+
const reason = await preToolUseUnansweredBlock(`this call to "${toolName}"`, unanswered);
|
|
448
|
+
await notifyPermissionDeniedSeat({ toolName, input: cloneObserverInput(currentInput), toolCallId, reason, source: "hook", ...(input.identity !== undefined ? { identity: input.identity } : {}) });
|
|
310
449
|
return { block: true, reason: formatHookFeedback(reason, input.reminderMark), preToolContext };
|
|
311
450
|
}
|
|
312
451
|
if (r) {
|
|
@@ -548,7 +687,7 @@ export async function runToolGate(input) {
|
|
|
548
687
|
catch {
|
|
549
688
|
return { unreadable: true };
|
|
550
689
|
}
|
|
551
|
-
return await pendingHit.then(normalizePersistedRuleHit).catch(() => ({ unreadable: true }));
|
|
690
|
+
return await settleOrgVerdictWithin(pendingHit.then(normalizePersistedRuleHit).catch(() => ({ unreadable: true })), { unreadable: true }, { ...(input.abortSignal !== undefined ? { signal: input.abortSignal } : {}), timeoutMs: ORG_ADJUDICATION_TIMEOUT_MS });
|
|
552
691
|
})();
|
|
553
692
|
const hitEntry = answer.hit;
|
|
554
693
|
const hit = hitEntry?.rule;
|
|
@@ -714,8 +853,13 @@ export async function runToolGate(input) {
|
|
|
714
853
|
}
|
|
715
854
|
if (preToolUse) {
|
|
716
855
|
let hr;
|
|
856
|
+
let hrUnanswered;
|
|
717
857
|
try {
|
|
718
|
-
|
|
858
|
+
const seat = await runHookSeat("preToolUse", seatBound(true), (sig) => preToolUse(toolName, editArgs, hookCtx(sig)));
|
|
859
|
+
if (seat.expired)
|
|
860
|
+
hrUnanswered = seat.cause;
|
|
861
|
+
else
|
|
862
|
+
hr = screenPreToolUseResult(seat.value);
|
|
719
863
|
}
|
|
720
864
|
catch (err) {
|
|
721
865
|
traceHookCrash(input, err, notifier);
|
|
@@ -723,6 +867,11 @@ export async function runToolGate(input) {
|
|
|
723
867
|
denySource = "hook";
|
|
724
868
|
break;
|
|
725
869
|
}
|
|
870
|
+
if (hrUnanswered !== undefined) {
|
|
871
|
+
editDenied = { action: "deny", message: await preToolUseUnansweredBlock(`the approved edit for "${toolName}"`, hrUnanswered) };
|
|
872
|
+
denySource = "hook";
|
|
873
|
+
break;
|
|
874
|
+
}
|
|
726
875
|
if (hr) {
|
|
727
876
|
if (hr.additionalContext)
|
|
728
877
|
preToolContext.push(hr.additionalContext);
|
|
@@ -828,9 +977,7 @@ export async function runToolGate(input) {
|
|
|
828
977
|
currentInput = decision.updatedInput;
|
|
829
978
|
}
|
|
830
979
|
const denyResolution = askDenyResolution ?? coreMintedResolutionOf(decision, { toolCallId, toolName });
|
|
831
|
-
|
|
832
|
-
await notifier.notifyAsync(() => input.permissionDenied?.({ toolName, input: cloneObserverInput(currentInput), toolCallId, reason: denyReason, source: denySource, ...(denyResolution !== undefined ? { resolution: denyResolution } : {}), ...(input.identity !== undefined ? { identity: input.identity } : {}) }), "toolGate.permissionDenied");
|
|
833
|
-
}
|
|
980
|
+
await notifyPermissionDeniedSeat({ toolName, input: cloneObserverInput(currentInput), toolCallId, reason: denyReason, source: denySource, ...(denyResolution !== undefined ? { resolution: denyResolution } : {}), ...(input.identity !== undefined ? { identity: input.identity } : {}) });
|
|
834
981
|
const denySettledBy = decision.settledBy;
|
|
835
982
|
const denyApprover = denySettledBy !== undefined ? resolvedApprover : undefined;
|
|
836
983
|
return {
|
|
@@ -4,8 +4,13 @@
|
|
|
4
4
|
* One renderer covers every human-input carrier: the core-side five (objective / live steer /
|
|
5
5
|
* nextTurn / the resume tail's parked-steer frames / the wake message) and a serving layer's own
|
|
6
6
|
* carriers through the same contract. The division of labor is fixed (ruled 2026-08-05): the INGRESS
|
|
7
|
-
* sanitizes text and mints the {@link ActorAssertion};
|
|
8
|
-
* speaker label ahead of the text, and it never sanitizes, truncates, or rewrites the text itself.
|
|
7
|
+
* sanitizes text and mints the {@link ActorAssertion}; `projectHumanInput` only PROJECTS — it renders
|
|
8
|
+
* the speaker label ahead of the text, and it never sanitizes, truncates, or rewrites the text itself.
|
|
9
|
+
*
|
|
10
|
+
* The module also owns the DELIVERY frame that tells the model where a human frame arrived
|
|
11
|
+
* ({@link frameMidTurnUserInput}, backlog #389). That is a separate concern from attribution and runs
|
|
12
|
+
* AFTER projection (the speaker label belongs to the text; the delivery frame belongs around it) — it is
|
|
13
|
+
* housed here so every model-facing wrapper for human input has one home.
|
|
9
14
|
*
|
|
10
15
|
* Envelope contract:
|
|
11
16
|
* - no `actor` ⇒ BYTE-IDENTICAL passthrough. Every pre-171 caller and every single-user host
|
|
@@ -35,6 +40,19 @@ export interface HumanInputFrame {
|
|
|
35
40
|
actor?: ActorAssertion;
|
|
36
41
|
source: HumanInputSource;
|
|
37
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* backlog #389 伴生 (V-3 of the steer/interrupt/lifecycle anchor audit) — wrap ONE live caller steer in
|
|
45
|
+
* the mid-turn delivery frame.
|
|
46
|
+
*
|
|
47
|
+
* Scope, deliberately: the UNTRUSTED caller lane only. A `trusted` steer already rides a
|
|
48
|
+
* `<system-reminder>` (supervisor authority, CC's own non-user-source framing), and a delegated child's
|
|
49
|
+
* operator steer carries its own explicit operator frame — the bare lane was exactly the most-travelled
|
|
50
|
+
* one: a person typing while the run works.
|
|
51
|
+
*
|
|
52
|
+
* Static strings (no per-call interpolation), and a blank projection is returned VERBATIM so the
|
|
53
|
+
* harness's empty-injection no-op stays a no-op — a frame must never materialize from nothing.
|
|
54
|
+
*/
|
|
55
|
+
export declare function frameMidTurnUserInput(projected: string): string;
|
|
38
56
|
/**
|
|
39
57
|
* Render the speaker envelope for one human-input frame. Pure; see the module contract above.
|
|
40
58
|
* Call BEFORE any trust framing (`formatHookFeedback` / `delimitUntrusted`) so the label stays
|
|
@@ -2,6 +2,15 @@ import { snapshotActorAssertion } from "../internal/llm.js";
|
|
|
2
2
|
import { uuidv7 } from "../internal/harness.js";
|
|
3
3
|
import { inlineUntrusted } from "./untrusted-text.js";
|
|
4
4
|
import { attrEscape, EXTERNAL_SOURCE_MAX } from "./task-notification.js";
|
|
5
|
+
const MID_TURN_USER_INPUT_HEAD = "The user sent a new message while you were working:\n";
|
|
6
|
+
const MID_TURN_USER_INPUT_TAIL = "\n\nThis is how a message sent mid-turn is surfaced: within the turn that is still running, often " +
|
|
7
|
+
"alongside the next tool result, rather than as a separate conversation turn. Address the message " +
|
|
8
|
+
"above as you continue this turn.";
|
|
9
|
+
export function frameMidTurnUserInput(projected) {
|
|
10
|
+
if (projected.trim().length === 0)
|
|
11
|
+
return projected;
|
|
12
|
+
return `${MID_TURN_USER_INPUT_HEAD}${projected}${MID_TURN_USER_INPUT_TAIL}`;
|
|
13
|
+
}
|
|
5
14
|
export function projectHumanInput(frame) {
|
|
6
15
|
if (frame.actor === undefined || frame.source === "system")
|
|
7
16
|
return frame.text;
|
|
@@ -1,19 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* design/121 — LSP diagnostics registry + model-facing formatting (CC 2.1.198 parity).
|
|
3
|
-
*
|
|
4
|
-
* CC's two diagnostics sources (IDE MCP baseline/diff + passive LSP publishDiagnostics) collapse to
|
|
5
|
-
* ONE in sema: the passive registry. The "only NEW diagnostics" semantics CC gets from per-file
|
|
6
|
-
* baselines falls out of the delivered-set here — a diagnostic is injected at most once PER RUN, and a
|
|
7
|
-
* run editing a file clears its delivered set so a persisting problem can resurface (CC `Fjn` same
|
|
8
|
-
* behavior). The registry object itself is deployment-scoped, so "per run" is a keyed fact, not an
|
|
9
|
-
* object lifetime; see {@link LspDiagnosticsRegistry}.
|
|
10
|
-
*
|
|
11
|
-
* Volumes and wire format are CC-exact (198:320661 `Njn=10, nqa=30`; 198:320480-320505 summary +
|
|
12
|
-
* 4000-char cap; severity symbols ✖/⚠/ℹ/★).
|
|
13
|
-
*
|
|
14
|
-
* NOT durable: diagnostics regenerate from the language server on the next edit; a suspend/resume
|
|
15
|
-
* simply starts empty (recorded in design/121 §2).
|
|
16
|
-
*/
|
|
17
1
|
/** One LSP diagnostic, the subset the model/shell needs (LSP `Diagnostic` narrowed). */
|
|
18
2
|
export interface LspDiagnostic {
|
|
19
3
|
message: string;
|
|
@@ -102,7 +86,25 @@ export declare class LspDiagnosticsRegistry {
|
|
|
102
86
|
* {symbol} [Line {line+1}:{col+1}] {message}[ [{code}]][ ({source})]
|
|
103
87
|
* ```
|
|
104
88
|
* capped at 4000 chars with an honest `…[truncated]` tail.
|
|
89
|
+
*
|
|
90
|
+
* ENVELOPE CONTAINMENT (untrusted-text.ts `ENGINE_ENVELOPES`, the `new-diagnostics` row): the
|
|
91
|
+
* per-diagnostic strings (`message`, `code`, `source`) and the file basename come from a LANGUAGE
|
|
92
|
+
* SERVER — a process the engine launches over repository content, i.e. the same trust class as the
|
|
93
|
+
* repository. They land inside the `<new-diagnostics>` envelope, and (since this block is now
|
|
94
|
+
* delivered inside a marked reminder shell) inside an authority-marked reminder too, so a diagnostic
|
|
95
|
+
* message carrying `</new-diagnostics>` closed the envelope and placed server text at the top level
|
|
96
|
+
* of engine-authored authority. CC 223 does not neutralize here either — a deliberate, disclosed
|
|
97
|
+
* hardening divergence: CC ships the same block inside the same reminder shell, so the hole exists on
|
|
98
|
+
* both sides and only the fix is ours. Neutralization runs BEFORE the join, so the 4000-char cap
|
|
99
|
+
* still bounds the FINAL text and can never cut inside a neutralized marker's own bytes.
|
|
105
100
|
*/
|
|
106
101
|
export declare function formatDiagnosticsSummary(files: LspFileDiagnostics[]): string;
|
|
107
|
-
/** CC 198 model-facing injection block (198:320500-320503), verbatim framing.
|
|
102
|
+
/** CC 198 model-facing injection block (198:320500-320503), verbatim framing.
|
|
103
|
+
*
|
|
104
|
+
* DELIVERY (design/319 sibling work): the runner shells this block in a MARKED `<system-reminder>`
|
|
105
|
+
* before steering it — CC 223 `case "diagnostics"` does the same (`ih([Vr({content: …})])` → the
|
|
106
|
+
* reminder wrap), and until that was matched this was the engine's only authority text reaching the
|
|
107
|
+
* model as a bare block with no provenance mark at all. The block's own bytes are unchanged; the
|
|
108
|
+
* shell is applied at the delivery site (runtask), not baked in here, so this exported renderer keeps
|
|
109
|
+
* serving callers that place the block themselves. */
|
|
108
110
|
export declare function formatDiagnosticsBlock(files: LspFileDiagnostics[]): string;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sanitizeUntrustedText, SHELLED_BODY_ENVELOPE_TAGS } from "./untrusted-text.js";
|
|
1
2
|
const MAX_PER_FILE = 10;
|
|
2
3
|
const MAX_TOTAL = 30;
|
|
3
4
|
const MAX_SUMMARY_CHARS = 4000;
|
|
@@ -79,14 +80,19 @@ function basenameOfUri(uri) {
|
|
|
79
80
|
return idx >= 0 ? path.slice(idx + 1) : path;
|
|
80
81
|
}
|
|
81
82
|
export function formatDiagnosticsSummary(files) {
|
|
83
|
+
const safe = (t) => sanitizeUntrustedText(t, SHELLED_BODY_ENVELOPE_TAGS);
|
|
82
84
|
const lines = [];
|
|
83
85
|
for (const f of files) {
|
|
84
|
-
lines.push(`${basenameOfUri(f.uri)}:`);
|
|
86
|
+
lines.push(`${safe(basenameOfUri(f.uri))}:`);
|
|
85
87
|
for (const d of f.diagnostics) {
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
|
|
88
|
+
const startLine = d.range?.start?.line;
|
|
89
|
+
const startChar = d.range?.start?.character;
|
|
90
|
+
const pos = typeof startLine === "number" && Number.isFinite(startLine) && typeof startChar === "number" && Number.isFinite(startChar)
|
|
91
|
+
? `[Line ${Math.trunc(startLine) + 1}:${Math.trunc(startChar) + 1}] `
|
|
92
|
+
: "";
|
|
93
|
+
const code = d.code !== undefined ? ` [${safe(String(d.code))}]` : "";
|
|
94
|
+
const source = d.source ? ` (${safe(String(d.source))})` : "";
|
|
95
|
+
lines.push(` ${severitySymbol(d.severity)} ${pos}${safe(String(d.message))}${code}${source}`);
|
|
90
96
|
}
|
|
91
97
|
}
|
|
92
98
|
const summary = lines.join("\n");
|
package/dist/core/mcp.d.ts
CHANGED
|
@@ -488,6 +488,30 @@ interface McpContentItem {
|
|
|
488
488
|
};
|
|
489
489
|
}
|
|
490
490
|
export declare function mapContent(content: Array<McpContentItem>, serverName?: string, imageResizer?: McpImageResizer): Promise<Array<TextContent | ImageContent>>;
|
|
491
|
+
/**
|
|
492
|
+
* Connect to each MCP server, list its tools, and wrap them as AgentTools.
|
|
493
|
+
* Tools are namespaced `mcp__<server>__<tool>` (CC parity) to avoid collisions. The model sees each
|
|
494
|
+
* tool's real JSON-Schema (`inputSchema`), which the agent loop validates natively.
|
|
495
|
+
*
|
|
496
|
+
* Fail-open (design/29): a server that fails to connect or list tools is SKIPPED — its error is
|
|
497
|
+
* returned in `warnings`, the healthy servers still materialize. A single misconfigured server
|
|
498
|
+
* (bad stdio command, unreachable URL) never bricks the whole task.
|
|
499
|
+
*
|
|
500
|
+
* Call `dispose()` when the task finishes — connections are task-scoped, never persisted.
|
|
501
|
+
*/
|
|
502
|
+
/**
|
|
503
|
+
* Invisible / format / private-use / unassigned characters stripped out of every model-facing string a
|
|
504
|
+
* server advertises — CC `MZg` :144247. CC spells this as the property class BELOW followed by five
|
|
505
|
+
* explicit ranges (zero-width U+200B–U+200F, bidi embedding U+202A–U+202E, bidi isolates U+2066–U+2069,
|
|
506
|
+
* the BOM, and the BMP private-use area); on this engine's Unicode data every one of those code points
|
|
507
|
+
* is already `Cf` or `Co`, so the class alone is byte-equivalent to CC's expression and the source stays
|
|
508
|
+
* free of literal invisible characters (a file carrying them is a grep blind spot, and this repo has
|
|
509
|
+
* paid for that once already). The equivalence is asserted by a pin, not assumed — a Unicode-data change
|
|
510
|
+
* that moved any of those ranges out of the class would open exactly the hole this strips.
|
|
511
|
+
*
|
|
512
|
+
* `\p{Cc}` is deliberately NOT here: newlines and tabs are legitimate description formatting.
|
|
513
|
+
*/
|
|
514
|
+
export declare const MCP_INVISIBLE_TEXT_RE: RegExp;
|
|
491
515
|
/** One tool's schema-normalization outcome (CC 220 `zyo` @336616-336654's return shape). */
|
|
492
516
|
export type McpSchemaNormalizeResult = {
|
|
493
517
|
outcome: "unchanged";
|
|
@@ -537,6 +561,28 @@ export declare function normalizeMcpToolSchema(schema: unknown): McpSchemaNormal
|
|
|
537
561
|
* to catch, and this gate alone does not catch it (no type key ⇒ passes the root-type check; the
|
|
538
562
|
* combinator's own structure is legal JSON Schema ⇒ passes validateJsonSchemaShape too).
|
|
539
563
|
*/
|
|
564
|
+
/**
|
|
565
|
+
* A schema defect the operator must HEAR about but that must not cost the tool its mount — returns a
|
|
566
|
+
* core-authored sentence, or `undefined` when there is nothing to say.
|
|
567
|
+
*
|
|
568
|
+
* Today that is exactly one thing: a TOP-LEVEL parameter name outside `[a-zA-Z0-9_.-]{1,64}`. A name
|
|
569
|
+
* like `"user name"` is rejected at the Anthropic wire, and because the tool table rides EVERY request
|
|
570
|
+
* it takes the whole request down with it — while the operator previously got nothing at all pointing
|
|
571
|
+
* at the cause (no warning, no drop record, just a task that stopped working). CC checks the identical
|
|
572
|
+
* names against the identical expression (`EX_`/`_xo` :325793/:325824, reported as `check:"propertyKey"`).
|
|
573
|
+
*
|
|
574
|
+
* WHY AN ADVISORY AND NOT A DROP, stated because the sibling structural gate above does drop: CC's own
|
|
575
|
+
* disposition here is keep-and-warn — its drop arm sits behind a rollout flag that ships OFF (`Xyd` /
|
|
576
|
+
* `Yyd` :354641/:354622, an empty remote-config list ⇒ false), so CC's shipped behavior is to keep the
|
|
577
|
+
* tool and warn "requests that include it may fail". The difference from the structural half is real
|
|
578
|
+
* and not a technicality: a schema with no object root is unusable everywhere, whereas this charset is
|
|
579
|
+
* ONE provider's rule, and this engine is bring-your-own-model. Dropping here would delete a working
|
|
580
|
+
* tool from a deployment whose provider accepts the name — a Chinese- or Japanese-named parameter is
|
|
581
|
+
* the ordinary case, not a hostile one. Detection was the gap; removal was never the mandate.
|
|
582
|
+
*
|
|
583
|
+
* TOP-LEVEL only, like CC: a nested property name is not what the provider validates.
|
|
584
|
+
*/
|
|
585
|
+
export declare function mcpToolSchemaAdvisory(schema: unknown): string | undefined;
|
|
540
586
|
export declare function mcpToolSchemaProblem(schema: unknown): string | undefined;
|
|
541
587
|
export declare function materializeMcpTools(specs: McpServerSpec[], principal?: string, onElicit?: OnElicit, imageResizer?: McpImageResizer, // design/116 CONFIRM-1 seam: deployment-injected; default = auto-detected sharp
|
|
542
588
|
reminderDisclosure?: {
|
package/dist/core/mcp.js
CHANGED
|
@@ -537,6 +537,82 @@ export async function mapContent(content, serverName, imageResizer) {
|
|
|
537
537
|
}
|
|
538
538
|
return out;
|
|
539
539
|
}
|
|
540
|
+
export const MCP_INVISIBLE_TEXT_RE = /[\p{Cf}\p{Co}\p{Cn}]/gu;
|
|
541
|
+
const MCP_SANITIZE_MAX_ROUNDS = 10;
|
|
542
|
+
function sanitizeMcpModelFacingText(text) {
|
|
543
|
+
let out = text;
|
|
544
|
+
for (let round = 0; round < MCP_SANITIZE_MAX_ROUNDS; round++) {
|
|
545
|
+
const next = out.normalize("NFKC").replace(MCP_INVISIBLE_TEXT_RE, "");
|
|
546
|
+
if (next === out)
|
|
547
|
+
return out;
|
|
548
|
+
out = next;
|
|
549
|
+
}
|
|
550
|
+
return out.replace(MCP_INVISIBLE_TEXT_RE, "");
|
|
551
|
+
}
|
|
552
|
+
const MCP_SCHEMA_PROSE_KEYS = new Set(["description", "title", "$comment"]);
|
|
553
|
+
const MCP_SCHEMA_CHILD_SEAT = new Map([
|
|
554
|
+
...["properties", "definitions", "$defs", "dependentSchemas"].map((k) => [k, "properties"]),
|
|
555
|
+
...["patternProperties"].map((k) => [k, "schemaMap"]),
|
|
556
|
+
...["items", "prefixItems", "additionalItems", "additionalProperties", "unevaluatedItems", "unevaluatedProperties", "contains", "propertyNames", "contentSchema", "not", "if", "then", "else", "allOf", "anyOf", "oneOf"].map((k) => [k, "schema"]),
|
|
557
|
+
...["required", "dependentRequired", "dependencies", "$ref", "$dynamicRef", "$id", "$anchor", "$dynamicAnchor"].map((k) => [k, "nameRefs"]),
|
|
558
|
+
]);
|
|
559
|
+
class McpPayloadKeyCollision extends Error {
|
|
560
|
+
}
|
|
561
|
+
function sanitizeMcpSchemaNameRefs(value) {
|
|
562
|
+
if (typeof value === "string")
|
|
563
|
+
return sanitizeMcpModelFacingText(value);
|
|
564
|
+
if (Array.isArray(value))
|
|
565
|
+
return value.map(sanitizeMcpSchemaNameRefs);
|
|
566
|
+
if (value !== null && typeof value === "object") {
|
|
567
|
+
const out = Object.create(null);
|
|
568
|
+
for (const [key, v] of Object.entries(value)) {
|
|
569
|
+
const neutralized = sanitizeMcpModelFacingText(key);
|
|
570
|
+
if (Object.hasOwn(out, neutralized))
|
|
571
|
+
throw new McpPayloadKeyCollision(neutralized);
|
|
572
|
+
out[neutralized] = Array.isArray(v) ? sanitizeMcpSchemaNameRefs(v) : sanitizeMcpModelFacingValue(v, "schema");
|
|
573
|
+
}
|
|
574
|
+
return out;
|
|
575
|
+
}
|
|
576
|
+
return value;
|
|
577
|
+
}
|
|
578
|
+
function sanitizeMcpModelFacingValue(value, seat = "schema") {
|
|
579
|
+
if (seat === "data")
|
|
580
|
+
return value;
|
|
581
|
+
if (seat === "nameRefs")
|
|
582
|
+
return sanitizeMcpSchemaNameRefs(value);
|
|
583
|
+
if (Array.isArray(value))
|
|
584
|
+
return value.map((v) => sanitizeMcpModelFacingValue(v, seat === "schema" ? "schema" : "data"));
|
|
585
|
+
if (value !== null && typeof value === "object") {
|
|
586
|
+
const out = Object.create(null);
|
|
587
|
+
for (const [key, v] of Object.entries(value)) {
|
|
588
|
+
if (seat === "schemaMap") {
|
|
589
|
+
out[key] = sanitizeMcpModelFacingValue(v, "schema");
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
const neutralized = sanitizeMcpModelFacingText(key);
|
|
593
|
+
if (Object.hasOwn(out, neutralized))
|
|
594
|
+
throw new McpPayloadKeyCollision(neutralized);
|
|
595
|
+
if (seat === "properties") {
|
|
596
|
+
out[neutralized] = sanitizeMcpModelFacingValue(v, "schema");
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
if (MCP_SCHEMA_PROSE_KEYS.has(neutralized)) {
|
|
600
|
+
out[neutralized] = typeof v === "string" ? sanitizeMcpModelFacingText(v) : v;
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
out[neutralized] = sanitizeMcpModelFacingValue(v, MCP_SCHEMA_CHILD_SEAT.get(neutralized) ?? "data");
|
|
604
|
+
}
|
|
605
|
+
return out;
|
|
606
|
+
}
|
|
607
|
+
return value;
|
|
608
|
+
}
|
|
609
|
+
const MCP_TOOL_DESCRIPTION_MAX_CHARS = 2048;
|
|
610
|
+
const MCP_TOOL_DESCRIPTION_TRUNCATION_MARK = "… [truncated]";
|
|
611
|
+
function capMcpToolDescription(description) {
|
|
612
|
+
if (description.length <= MCP_TOOL_DESCRIPTION_MAX_CHARS)
|
|
613
|
+
return description;
|
|
614
|
+
return sliceHeadSafe(description, MCP_TOOL_DESCRIPTION_MAX_CHARS) + MCP_TOOL_DESCRIPTION_TRUNCATION_MARK;
|
|
615
|
+
}
|
|
540
616
|
const MCP_SCHEMA_COMBINATOR_KEYS = ["anyOf", "oneOf", "allOf"];
|
|
541
617
|
const MCP_SCHEMA_PROP_NAME_RE = /^[a-zA-Z0-9_.-]{1,64}$/;
|
|
542
618
|
const MCP_SCHEMA_CARRY_KEYS = ["$defs", "definitions", "$schema", "additionalProperties", "description", "title"];
|
|
@@ -643,6 +719,19 @@ export function normalizeMcpToolSchema(schema) {
|
|
|
643
719
|
return { outcome: "drop", reason: `input schema uses top-level ${combinatorsPresent.join("/")} and could not be normalized` };
|
|
644
720
|
}
|
|
645
721
|
}
|
|
722
|
+
export function mcpToolSchemaAdvisory(schema) {
|
|
723
|
+
if (schema === undefined || !isPlainSchemaObject(schema))
|
|
724
|
+
return undefined;
|
|
725
|
+
const properties = schema.properties;
|
|
726
|
+
if (!isPlainSchemaObject(properties))
|
|
727
|
+
return undefined;
|
|
728
|
+
for (const key of Object.keys(properties)) {
|
|
729
|
+
if (!MCP_SCHEMA_PROP_NAME_RE.test(key)) {
|
|
730
|
+
return `its parameter name ${inlineUntrusted(JSON.stringify(key), 80)} is outside the character set some providers accept for tool parameters (${MCP_SCHEMA_PROP_NAME_RE.source}); requests carrying this tool may be rejected by such a provider`;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
return undefined;
|
|
734
|
+
}
|
|
646
735
|
export function mcpToolSchemaProblem(schema) {
|
|
647
736
|
if (schema === undefined)
|
|
648
737
|
return undefined;
|
|
@@ -741,6 +830,11 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
|
|
|
741
830
|
resourceServers.push(s.resourceServer);
|
|
742
831
|
for (const d of s.dropped)
|
|
743
832
|
droppedTools.push({ server: inlineUntrusted(spec.name, 160), ...d });
|
|
833
|
+
for (const a of s.schemaAdvisories)
|
|
834
|
+
warnings.push(schemaAdvisoryWarning(spec.name, a.tool, a.reason));
|
|
835
|
+
if (s.toolsCapabilityAbsent === true && spec.allowTools !== undefined && spec.allowTools.length > 0) {
|
|
836
|
+
warnings.push(toolsCapabilityAbsentWarning(spec.name));
|
|
837
|
+
}
|
|
744
838
|
if (s.listingIncomplete)
|
|
745
839
|
warnings.push(listingIncompleteWarning(spec.name, s.listingIncomplete));
|
|
746
840
|
statuses.push(s.status);
|
|
@@ -1329,6 +1423,8 @@ export function parseCallToolResultLenient(data) {
|
|
|
1329
1423
|
}
|
|
1330
1424
|
const LENIENT_CALL_TOOL_RESULT_SCHEMA = { safeParse: parseCallToolResultLenient };
|
|
1331
1425
|
async function listToolsLenient(client, options) {
|
|
1426
|
+
if (!client.getServerCapabilities()?.tools)
|
|
1427
|
+
return { tools: [], toolsCapabilityAbsent: true };
|
|
1332
1428
|
const walk = await walkMcpListPages(async (cursor, remainingMs) => {
|
|
1333
1429
|
const page = await client.request({ method: "tools/list", params: cursor === undefined ? {} : { cursor } }, LenientListToolsResultSchema, { ...options, timeout: remainingMs });
|
|
1334
1430
|
return {
|
|
@@ -1379,7 +1475,7 @@ async function connectServer(spec, principal, onElicit, imageResizer, reminderDi
|
|
|
1379
1475
|
};
|
|
1380
1476
|
const listed = await listToolsLenient(client, startupOpts);
|
|
1381
1477
|
cacheMcpToolMetadata(client, listed.tools);
|
|
1382
|
-
const { serverTools, serverAxes, dropped } = intakeListedTools(listed, spec, client, health, imageResizer, reminderDisclosure, isServerRevoked);
|
|
1478
|
+
const { serverTools, serverAxes, dropped, advisories } = intakeListedTools(listed, spec, client, health, imageResizer, reminderDisclosure, isServerRevoked);
|
|
1383
1479
|
const caps = client.getServerCapabilities();
|
|
1384
1480
|
const resourceInfo = caps?.resources
|
|
1385
1481
|
? {
|
|
@@ -1403,6 +1499,8 @@ async function connectServer(spec, principal, onElicit, imageResizer, reminderDi
|
|
|
1403
1499
|
tools: serverTools,
|
|
1404
1500
|
axes: serverAxes,
|
|
1405
1501
|
dropped,
|
|
1502
|
+
schemaAdvisories: advisories,
|
|
1503
|
+
...(listed.toolsCapabilityAbsent === true ? { toolsCapabilityAbsent: true } : {}),
|
|
1406
1504
|
listedTools: listed.tools,
|
|
1407
1505
|
...(instructions ? { instructions } : {}),
|
|
1408
1506
|
...(listed.incomplete !== undefined ? { listingIncomplete: listed.incomplete } : {}),
|
|
@@ -1418,14 +1516,28 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
|
|
|
1418
1516
|
const serverTools = [];
|
|
1419
1517
|
const serverAxes = [];
|
|
1420
1518
|
const dropped = [];
|
|
1519
|
+
const advisories = [];
|
|
1421
1520
|
const mintedNames = new Map();
|
|
1422
1521
|
for (const t of listed.tools) {
|
|
1423
1522
|
if (spec.allowTools && !spec.allowTools.includes(t.name)) {
|
|
1424
1523
|
continue;
|
|
1425
1524
|
}
|
|
1426
|
-
|
|
1427
|
-
let
|
|
1428
|
-
|
|
1525
|
+
let modelFacingSchema;
|
|
1526
|
+
let sanitizedDescription;
|
|
1527
|
+
try {
|
|
1528
|
+
modelFacingSchema = sanitizeMcpModelFacingValue(t.inputSchema);
|
|
1529
|
+
sanitizedDescription = t.description !== undefined ? sanitizeMcpModelFacingText(t.description) : undefined;
|
|
1530
|
+
}
|
|
1531
|
+
catch (err) {
|
|
1532
|
+
const reason = err instanceof McpPayloadKeyCollision
|
|
1533
|
+
? `two of its advertised names neutralize to the same model-facing spelling ${inlineUntrusted(JSON.stringify(err.message), 80)}; one parameter would have vanished from the schema the model is given while the server still expects it`
|
|
1534
|
+
: "advertised tool payload could not be neutralized for model-facing display (pathological nesting)";
|
|
1535
|
+
dropped.push({ tool: inlineUntrusted(t.name), reason: inlineUntrusted(reason, 240) });
|
|
1536
|
+
continue;
|
|
1537
|
+
}
|
|
1538
|
+
const normalized = normalizeMcpToolSchema(modelFacingSchema);
|
|
1539
|
+
let effectiveInputSchema = modelFacingSchema;
|
|
1540
|
+
let effectiveDescription = sanitizedDescription !== undefined ? sanitizeUntrustedText(sanitizedDescription) : undefined;
|
|
1429
1541
|
if (normalized.outcome === "drop") {
|
|
1430
1542
|
dropped.push({ tool: inlineUntrusted(t.name), reason: inlineUntrusted(normalized.reason, 240) });
|
|
1431
1543
|
continue;
|
|
@@ -1439,6 +1551,10 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
|
|
|
1439
1551
|
dropped.push({ tool: inlineUntrusted(t.name), reason: inlineUntrusted(schemaProblem, 240) });
|
|
1440
1552
|
continue;
|
|
1441
1553
|
}
|
|
1554
|
+
const schemaAdvisory = mcpToolSchemaAdvisory(effectiveInputSchema);
|
|
1555
|
+
if (schemaAdvisory !== undefined) {
|
|
1556
|
+
advisories.push({ tool: inlineUntrusted(t.name), reason: inlineUntrusted(schemaAdvisory, 240) });
|
|
1557
|
+
}
|
|
1442
1558
|
const remoteName = t.name;
|
|
1443
1559
|
const namespacedName = mintNamespacedToolName(MCP_NAMESPACE, spec.name, remoteName);
|
|
1444
1560
|
const mintedBy = mintedNames.get(namespacedName);
|
|
@@ -1462,7 +1578,7 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
|
|
|
1462
1578
|
const mcpAlwaysLoad = mcpToolMeta?.["anthropic/alwaysLoad"] === true;
|
|
1463
1579
|
serverTools.push({
|
|
1464
1580
|
name: namespacedName,
|
|
1465
|
-
description: effectiveDescription ?? `MCP tool ${remoteName} from ${spec.name}
|
|
1581
|
+
description: capMcpToolDescription(effectiveDescription ?? `MCP tool ${inlineUntrusted(sanitizeMcpModelFacingText(remoteName))} from ${spec.name}`),
|
|
1466
1582
|
label: `${spec.name}:${remoteName}`,
|
|
1467
1583
|
parameters: (effectiveInputSchema ?? { type: "object" }),
|
|
1468
1584
|
...(mcpMaxResultSizeChars !== undefined ? { mcpMaxResultSizeChars } : {}),
|
|
@@ -1564,13 +1680,23 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
|
|
|
1564
1680
|
},
|
|
1565
1681
|
});
|
|
1566
1682
|
}
|
|
1567
|
-
return { serverTools, serverAxes, dropped };
|
|
1683
|
+
return { serverTools, serverAxes, dropped, advisories };
|
|
1568
1684
|
}
|
|
1569
1685
|
function asServerWarning(spec, err) {
|
|
1570
1686
|
const warning = new Error(`mcp: server "${spec.name}" failed to connect — skipped (${namedMcpFailureText(err)})`, { cause: err });
|
|
1571
1687
|
warning.code = "mcp.server_unavailable";
|
|
1572
1688
|
return warning;
|
|
1573
1689
|
}
|
|
1690
|
+
function schemaAdvisoryWarning(server, tool, reason) {
|
|
1691
|
+
const warning = new Error(`mcp: server "${inlineUntrusted(server, 160)}" tool "${tool}" is mounted, but ${reason}.`);
|
|
1692
|
+
warning.code = "mcp.tool_schema_advisory";
|
|
1693
|
+
return warning;
|
|
1694
|
+
}
|
|
1695
|
+
function toolsCapabilityAbsentWarning(server) {
|
|
1696
|
+
const warning = new Error(`mcp: server "${inlineUntrusted(server, 160)}" declared no "tools" capability, so no tool listing was requested and none of the tools named in this server's allowTools can be mounted. If the server does serve tools, it must declare the capability at initialize.`);
|
|
1697
|
+
warning.code = "mcp.tools_capability_absent";
|
|
1698
|
+
return warning;
|
|
1699
|
+
}
|
|
1574
1700
|
function listingIncompleteWarning(server, flag) {
|
|
1575
1701
|
const warning = new Error(`mcp: server "${inlineUntrusted(server, 160)}" listed its tools INCOMPLETELY — ${listingIncompleteNote(flag)}. The tools beyond the ${flag.pages} page${flag.pages === 1 ? "" : "s"} retrieved are NOT mounted for this task.`);
|
|
1576
1702
|
warning.code = "mcp.listing_incomplete";
|