@sema-agent/core 5.58.0 → 5.60.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 +88 -0
- package/dist/brain/anthropic.js +15 -5
- package/dist/brain/errors.d.ts +18 -1
- package/dist/brain/errors.js +7 -1
- package/dist/brain/input-too-long.d.ts +57 -0
- package/dist/brain/input-too-long.js +35 -0
- package/dist/brain/route-adjudicator.d.ts +8 -1
- package/dist/brain/route-adjudicator.js +8 -1
- package/dist/brain/stream-engine.js +9 -1
- package/dist/core/auto-compaction.js +2 -2
- package/dist/core/checkpoint-store.d.ts +116 -19
- package/dist/core/checkpoint-store.js +15 -8
- package/dist/core/context-edit.d.ts +243 -41
- package/dist/core/context-edit.js +247 -32
- package/dist/core/governance-codes.d.ts +37 -10
- package/dist/core/governance-codes.js +57 -1
- package/dist/core/locked-config.d.ts +36 -4
- package/dist/core/locked-config.js +34 -1
- package/dist/core/mcp.js +10 -6
- package/dist/core/memory-engine/consolidation-driver.d.ts +6 -2
- package/dist/core/memory-engine/consolidation-driver.js +54 -5
- package/dist/core/memory-engine/consolidation.d.ts +73 -1
- package/dist/core/memory-engine/consolidation.js +21 -1
- package/dist/core/memory-engine/content-origin.d.ts +24 -2
- package/dist/core/memory-engine/content-origin.js +6 -1
- package/dist/core/memory-engine/engine.d.ts +97 -8
- package/dist/core/memory-engine/engine.js +112 -20
- package/dist/core/memory-engine/file-backend.d.ts +13 -1
- package/dist/core/memory-engine/file-backend.js +3 -0
- package/dist/core/memory-engine/index.d.ts +4 -3
- package/dist/core/memory-engine/layout.js +20 -6
- package/dist/core/memory-engine/types.d.ts +17 -0
- package/dist/core/memory.d.ts +10 -0
- package/dist/core/park-selfcheck.js +1 -0
- package/dist/core/permission-rule-consent.js +9 -5
- package/dist/core/permission-rule-model.d.ts +42 -1
- package/dist/core/permission-rule-model.js +12 -0
- package/dist/core/runner/prepare-config-doors.d.ts +22 -1
- package/dist/core/runner/prepare-config-doors.js +36 -0
- package/dist/core/runner/prepare-task.d.ts +28 -1
- package/dist/core/runner/prepare-task.js +109 -11
- package/dist/core/runner/runtask.js +45 -8
- package/dist/core/store-contracts/checkpoint-store-contract.js +32 -0
- package/dist/core/tool-policy.d.ts +74 -0
- package/dist/core/tool-policy.js +80 -1
- package/dist/core/tools.js +1 -1
- package/dist/core/trace.d.ts +36 -0
- package/dist/core/types.d.ts +172 -22
- package/dist/core/types.js +4 -3
- package/dist/core/untrusted-text.d.ts +11 -0
- package/dist/core/untrusted-text.js +1 -0
- package/dist/engine/llm/types.d.ts +21 -2
- package/dist/engine/loop/agent-loop.js +7 -1
- package/dist/engine/loop/types.d.ts +4 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/tools/fs/fs-bash.js +1 -2
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +1771 -1
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { DEFAULT_CHARS_PER_TOKEN, estimateContextTokens, estimateTokens } from "../internal/harness.js";
|
|
2
3
|
import { isToolResult } from "./message-utils.js";
|
|
3
|
-
import { offloadPagebackHint } from "./tool-result-store.js";
|
|
4
|
+
import { PERSISTED_OUTPUT_PREFIX, offloadPagebackHint } from "./tool-result-store.js";
|
|
4
5
|
const CLEARED_MARKER = "[tool result cleared to save context]";
|
|
6
|
+
const CC_CLEARED_MARKER = "[Old tool result content cleared]";
|
|
7
|
+
const LEGACY_CLEARED_EXTENDED_PREFIX = "[tool result cleared to save context —";
|
|
8
|
+
const CC_CLEARED_EXTENDED_PREFIX = "[Old tool result content cleared —";
|
|
5
9
|
const refNote = (ref) => offloadPagebackHint(ref, "cleared");
|
|
6
10
|
const mediaNote = (blocks) => {
|
|
7
11
|
const byType = new Map();
|
|
@@ -17,9 +21,10 @@ const mediaNote = (blocks) => {
|
|
|
17
21
|
const breakdown = [...byType].map(([t, c]) => `${c} ${t}`).join(", ");
|
|
18
22
|
return `${n} attachments (${breakdown}) no longer visible after this clear`;
|
|
19
23
|
};
|
|
20
|
-
const clearedMarker = (notes) => {
|
|
24
|
+
const clearedMarker = (notes, machine = "legacy") => {
|
|
21
25
|
const present = notes.filter((n) => n !== undefined);
|
|
22
|
-
|
|
26
|
+
const [plain, extendedPrefix] = machine === "cc" ? [CC_CLEARED_MARKER, CC_CLEARED_EXTENDED_PREFIX] : [CLEARED_MARKER, LEGACY_CLEARED_EXTENDED_PREFIX];
|
|
27
|
+
return present.length > 0 ? `${extendedPrefix} ${present.join("; ")}]` : plain;
|
|
23
28
|
};
|
|
24
29
|
export const EDIT_FRACTION = 0.7;
|
|
25
30
|
export const CONTEXT_OUTPUT_RESERVE_TOKENS = 20000;
|
|
@@ -28,13 +33,16 @@ export function contextEditFrontier(window) {
|
|
|
28
33
|
return Math.max(window - (CONTEXT_OUTPUT_RESERVE_TOKENS + COMPACTION_TRIGGER_BUFFER_TOKENS), Math.floor(window * EDIT_FRACTION));
|
|
29
34
|
}
|
|
30
35
|
export const DEFAULT_KEEP_RECENT_TOOL_RESULTS = 3;
|
|
36
|
+
export const CC_DEFAULT_KEEP_RECENT_TOOL_RESULTS = 5;
|
|
37
|
+
export const MIN_CLEAR_SAVINGS_TOKENS = 20000;
|
|
38
|
+
const MEDIA_BLOCK_SAVINGS_TOKENS = 2000;
|
|
31
39
|
export const MIN_KEEP_RECENT_TOOL_RESULTS = 1;
|
|
32
|
-
function resolveKeepRecentToolResults(value) {
|
|
40
|
+
function resolveKeepRecentToolResults(value, defaultValue) {
|
|
33
41
|
if (value === undefined)
|
|
34
|
-
return
|
|
42
|
+
return defaultValue;
|
|
35
43
|
if (!Number.isSafeInteger(value) || value < 0) {
|
|
36
44
|
throw new TypeError(`keepRecentToolResults must be a non-negative safe integer (got ${String(value)}); ` +
|
|
37
|
-
`omit it for the default ${
|
|
45
|
+
`omit it for the default ${defaultValue}`);
|
|
38
46
|
}
|
|
39
47
|
return Math.max(MIN_KEEP_RECENT_TOOL_RESULTS, value);
|
|
40
48
|
}
|
|
@@ -49,12 +57,29 @@ export const COMPACTABLE_TOOLS = new Set([
|
|
|
49
57
|
"Edit",
|
|
50
58
|
"Write",
|
|
51
59
|
]);
|
|
52
|
-
function
|
|
60
|
+
function soleMarkerText(m) {
|
|
53
61
|
const content = m.content;
|
|
54
62
|
if (!Array.isArray(content) || content.length !== 1)
|
|
55
|
-
return
|
|
63
|
+
return undefined;
|
|
56
64
|
const text = content[0].text;
|
|
57
|
-
return
|
|
65
|
+
return typeof text === "string" ? text : undefined;
|
|
66
|
+
}
|
|
67
|
+
function isCleared(m, recognizeCcForms = false) {
|
|
68
|
+
const text = soleMarkerText(m);
|
|
69
|
+
if (text === undefined)
|
|
70
|
+
return false;
|
|
71
|
+
if (text === CLEARED_MARKER || text.startsWith(LEGACY_CLEARED_EXTENDED_PREFIX))
|
|
72
|
+
return true;
|
|
73
|
+
return recognizeCcForms && (text === CC_CLEARED_MARKER || text.startsWith(CC_CLEARED_EXTENDED_PREFIX));
|
|
74
|
+
}
|
|
75
|
+
function isCcCleared(m) {
|
|
76
|
+
if (isCleared(m, true))
|
|
77
|
+
return true;
|
|
78
|
+
const content = m.content;
|
|
79
|
+
if (!Array.isArray(content) || content.length === 0)
|
|
80
|
+
return false;
|
|
81
|
+
const first = content[0];
|
|
82
|
+
return first?.type === "text" && typeof first.text === "string" && first.text.startsWith(PERSISTED_OUTPUT_PREFIX);
|
|
58
83
|
}
|
|
59
84
|
export function dropEmptyFailureAssistants(messages) {
|
|
60
85
|
const isJunk = (m) => {
|
|
@@ -67,8 +92,56 @@ export function dropEmptyFailureAssistants(messages) {
|
|
|
67
92
|
};
|
|
68
93
|
return messages.some(isJunk) ? messages.filter((m) => !isJunk(m)) : messages;
|
|
69
94
|
}
|
|
95
|
+
function structuralClearSavings(target) {
|
|
96
|
+
const content = Array.isArray(target.content) ? target.content : [];
|
|
97
|
+
let saved = 0;
|
|
98
|
+
for (const c of content) {
|
|
99
|
+
const block = c;
|
|
100
|
+
if (block?.type === "text") {
|
|
101
|
+
saved += typeof block.text === "string" ? Math.round(block.text.length / 4) : 0;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
saved += MEDIA_BLOCK_SAVINGS_TOKENS;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return saved;
|
|
108
|
+
}
|
|
109
|
+
function clearOneResult(target, offload, machine) {
|
|
110
|
+
const rawContent = Array.isArray(target.content) ? target.content : [];
|
|
111
|
+
let ref;
|
|
112
|
+
if (offload) {
|
|
113
|
+
const fullText = rawContent
|
|
114
|
+
.filter((c) => c?.type === "text" && typeof c.text === "string")
|
|
115
|
+
.map((c) => c.text)
|
|
116
|
+
.join("\n");
|
|
117
|
+
if (target.toolCallId && fullText.trim().length > 0) {
|
|
118
|
+
try {
|
|
119
|
+
ref = refNote(offload.persist(target.toolCallId, fullText));
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
ref = undefined;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const mediaBlocks = rawContent.filter((c) => c?.type !== "text");
|
|
127
|
+
const marker = clearedMarker([ref, mediaBlocks.length > 0 ? mediaNote(mediaBlocks) : undefined], machine);
|
|
128
|
+
return { ...target, content: [{ type: "text", text: marker }] };
|
|
129
|
+
}
|
|
130
|
+
function reportCleared(onCleared, clears, tokensSavedEstimate) {
|
|
131
|
+
if (!onCleared || clears.length === 0)
|
|
132
|
+
return;
|
|
133
|
+
try {
|
|
134
|
+
onCleared({ clears, tokensSavedEstimate });
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
}
|
|
138
|
+
}
|
|
70
139
|
export function clearStaleToolResults(messages, opts) {
|
|
71
140
|
const cpt = opts.charsPerToken ?? DEFAULT_CHARS_PER_TOKEN;
|
|
141
|
+
const machine = opts.machine ?? "legacy";
|
|
142
|
+
if (machine !== "legacy" && machine !== "cc") {
|
|
143
|
+
throw new TypeError(`machine must be "legacy" | "cc" (got ${String(machine)}); omit it for the default "legacy"`);
|
|
144
|
+
}
|
|
72
145
|
const anchored = opts.anchoredTotalTokens !== undefined;
|
|
73
146
|
let total;
|
|
74
147
|
let anchorIdx = -1;
|
|
@@ -85,48 +158,190 @@ export function clearStaleToolResults(messages, opts) {
|
|
|
85
158
|
if (total <= opts.budgetTokens) {
|
|
86
159
|
return messages;
|
|
87
160
|
}
|
|
88
|
-
|
|
161
|
+
if (machine === "cc") {
|
|
162
|
+
return clearStaleToolResultsCc(messages, opts);
|
|
163
|
+
}
|
|
164
|
+
const keep = resolveKeepRecentToolResults(opts.keepRecentToolResults, DEFAULT_KEEP_RECENT_TOOL_RESULTS);
|
|
89
165
|
const compactable = opts.compactableTools ?? COMPACTABLE_TOOLS;
|
|
90
|
-
const toolResultCandidates = messages.flatMap((m, i) => isToolResult(m) && !isCleared(m) && compactable.has(m.toolName) ? [{ idx: i, target: m }] : []);
|
|
166
|
+
const toolResultCandidates = messages.flatMap((m, i) => isToolResult(m) && !isCleared(m, opts.recognizeCcMarkers === true) && compactable.has(m.toolName) ? [{ idx: i, target: m }] : []);
|
|
91
167
|
const clearable = toolResultCandidates.slice(0, Math.max(0, toolResultCandidates.length - keep));
|
|
92
168
|
if (clearable.length === 0) {
|
|
93
169
|
return messages;
|
|
94
170
|
}
|
|
95
171
|
const out = messages.slice();
|
|
96
172
|
let current = total;
|
|
173
|
+
const clears = [];
|
|
174
|
+
let savedEstimate = 0;
|
|
97
175
|
for (const { idx, target } of clearable) {
|
|
98
176
|
if (current <= opts.budgetTokens) {
|
|
99
177
|
break;
|
|
100
178
|
}
|
|
101
179
|
const before = estimateTokens(target, cpt);
|
|
102
|
-
const
|
|
103
|
-
let ref;
|
|
104
|
-
if (opts.offload) {
|
|
105
|
-
const fullText = rawContent
|
|
106
|
-
.filter((c) => c?.type === "text" && typeof c.text === "string")
|
|
107
|
-
.map((c) => c.text)
|
|
108
|
-
.join("\n");
|
|
109
|
-
if (target.toolCallId && fullText.trim().length > 0) {
|
|
110
|
-
try {
|
|
111
|
-
ref = refNote(opts.offload.persist(target.toolCallId, fullText));
|
|
112
|
-
}
|
|
113
|
-
catch {
|
|
114
|
-
ref = undefined;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
const mediaBlocks = rawContent.filter((c) => c?.type !== "text");
|
|
119
|
-
const marker = clearedMarker([ref, mediaBlocks.length > 0 ? mediaNote(mediaBlocks) : undefined]);
|
|
120
|
-
const cleared = { ...target, content: [{ type: "text", text: marker }] };
|
|
180
|
+
const cleared = clearOneResult(target, opts.offload, "legacy");
|
|
121
181
|
out[idx] = cleared;
|
|
122
|
-
|
|
182
|
+
clears.push({ index: idx });
|
|
183
|
+
const delta = before - estimateTokens(cleared, cpt);
|
|
184
|
+
savedEstimate += delta;
|
|
185
|
+
current -= idx > anchorIdx ? delta : 0;
|
|
123
186
|
}
|
|
187
|
+
reportCleared(opts.onCleared, clears, savedEstimate);
|
|
124
188
|
return out;
|
|
125
189
|
}
|
|
190
|
+
function clearStaleToolResultsCc(messages, opts) {
|
|
191
|
+
const keep = resolveKeepRecentToolResults(opts.keepRecentToolResults, CC_DEFAULT_KEEP_RECENT_TOOL_RESULTS);
|
|
192
|
+
const compactable = opts.compactableTools ?? COMPACTABLE_TOOLS;
|
|
193
|
+
const candidates = messages.flatMap((m, i) => isToolResult(m) && compactable.has(m.toolName) ? [{ idx: i, target: m }] : []);
|
|
194
|
+
const beyondKeep = candidates.slice(0, Math.max(0, candidates.length - keep));
|
|
195
|
+
const clearable = beyondKeep.filter(({ target }) => !isCcCleared(target));
|
|
196
|
+
if (clearable.length === 0) {
|
|
197
|
+
return messages;
|
|
198
|
+
}
|
|
199
|
+
const tokensSaved = clearable.reduce((sum, { target }) => sum + structuralClearSavings(target), 0);
|
|
200
|
+
if (tokensSaved < MIN_CLEAR_SAVINGS_TOKENS) {
|
|
201
|
+
return messages;
|
|
202
|
+
}
|
|
203
|
+
const out = messages.slice();
|
|
204
|
+
const clears = [];
|
|
205
|
+
for (const { idx, target } of clearable) {
|
|
206
|
+
const src = opts.clearSource?.[idx];
|
|
207
|
+
const source = src !== undefined && isToolResult(src) && src.toolCallId === target.toolCallId ? src : target;
|
|
208
|
+
out[idx] = clearOneResult(source, opts.offload, "cc");
|
|
209
|
+
clears.push({ index: idx });
|
|
210
|
+
}
|
|
211
|
+
reportCleared(opts.onCleared, clears, tokensSaved);
|
|
212
|
+
return out;
|
|
213
|
+
}
|
|
214
|
+
export function resolveTriggerWindow(model) {
|
|
215
|
+
const physical = model.contextTokens ?? model.contextWindow;
|
|
216
|
+
const declared = model.autoCompactTokens;
|
|
217
|
+
if (declared !== undefined &&
|
|
218
|
+
Number.isFinite(declared) &&
|
|
219
|
+
declared > 0 &&
|
|
220
|
+
Number.isFinite(physical) &&
|
|
221
|
+
physical > 0 &&
|
|
222
|
+
declared > physical) {
|
|
223
|
+
return { window: physical, clamped: true, declaredAutoCompactTokens: declared, physicalWindow: physical };
|
|
224
|
+
}
|
|
225
|
+
return { window: declared ?? physical, clamped: false };
|
|
226
|
+
}
|
|
126
227
|
export function editBudget(model) {
|
|
127
|
-
const window = model.
|
|
228
|
+
const window = resolveTriggerWindow(model).window;
|
|
128
229
|
if (!Number.isFinite(window) || window <= 0) {
|
|
129
230
|
return Number.POSITIVE_INFINITY;
|
|
130
231
|
}
|
|
131
232
|
return contextEditFrontier(window);
|
|
132
233
|
}
|
|
234
|
+
export function fingerprintToolResultOccurrence(m) {
|
|
235
|
+
const content = Array.isArray(m.content)
|
|
236
|
+
? (m.content)
|
|
237
|
+
: [];
|
|
238
|
+
const canonical = content.map((c) => {
|
|
239
|
+
const block = c;
|
|
240
|
+
if (block?.type === "text" && typeof block.text === "string") {
|
|
241
|
+
return ["text", block.text];
|
|
242
|
+
}
|
|
243
|
+
return [block?.type ?? "?", block?.mimeType ?? "", typeof block?.data === "string" ? block.data : ""];
|
|
244
|
+
});
|
|
245
|
+
return createHash("sha256").update(JSON.stringify(canonical)).digest("hex");
|
|
246
|
+
}
|
|
247
|
+
export function createClearedProjectionLedger() {
|
|
248
|
+
return { entries: new Map() };
|
|
249
|
+
}
|
|
250
|
+
const OCCURRENCE_KEY_SEP = String.fromCharCode(0);
|
|
251
|
+
function occurrenceGroupId(m) {
|
|
252
|
+
return `${m.toolCallId}${OCCURRENCE_KEY_SEP}${m.timestamp ?? 0}`;
|
|
253
|
+
}
|
|
254
|
+
export function planRejectionClears(messages, opts) {
|
|
255
|
+
const keep = resolveKeepRecentToolResults(opts.keepRecentToolResults, CC_DEFAULT_KEEP_RECENT_TOOL_RESULTS);
|
|
256
|
+
const compactable = opts.compactableTools ?? COMPACTABLE_TOOLS;
|
|
257
|
+
const candidates = messages.flatMap((m) => (isToolResult(m) && compactable.has(m.toolName) ? [m] : []));
|
|
258
|
+
const beyondKeep = candidates.slice(0, Math.max(0, candidates.length - keep));
|
|
259
|
+
const clearable = beyondKeep.flatMap((m) => {
|
|
260
|
+
if (isCcCleared(m))
|
|
261
|
+
return [];
|
|
262
|
+
const at = opts.keyOf(m);
|
|
263
|
+
return at === undefined ? [] : [{ m, at }];
|
|
264
|
+
});
|
|
265
|
+
if (clearable.length === 0)
|
|
266
|
+
return { declined: "no_candidates" };
|
|
267
|
+
const tokensSavedEstimate = clearable.reduce((sum, { m }) => sum + structuralClearSavings(m), 0);
|
|
268
|
+
if (tokensSavedEstimate < MIN_CLEAR_SAVINGS_TOKENS)
|
|
269
|
+
return { declined: "below_min_savings" };
|
|
270
|
+
const cleared = clearable.map(({ m, at }) => {
|
|
271
|
+
const source = at.occurrence !== undefined && isToolResult(at.occurrence) ? at.occurrence : m;
|
|
272
|
+
const clearedMsg = clearOneResult(source, opts.offload, "cc");
|
|
273
|
+
return { key: at.key, groupCount: at.groupCount, marker: clearedMsg.content[0]?.text ?? CC_CLEARED_MARKER, fp: at.fp };
|
|
274
|
+
});
|
|
275
|
+
return { cleared, tokensSavedEstimate };
|
|
276
|
+
}
|
|
277
|
+
export function replayClearedProjection(messages, ledger) {
|
|
278
|
+
const groups = new Map();
|
|
279
|
+
const byIndex = new Map();
|
|
280
|
+
const byObject = new WeakMap();
|
|
281
|
+
messages.forEach((m, i) => {
|
|
282
|
+
if (!isToolResult(m))
|
|
283
|
+
return;
|
|
284
|
+
const g = occurrenceGroupId(m);
|
|
285
|
+
const list = groups.get(g) ?? [];
|
|
286
|
+
byIndex.set(i, { group: g, dupIdx: list.length });
|
|
287
|
+
byObject.set(m, { group: g, dupIdx: list.length });
|
|
288
|
+
list.push(i);
|
|
289
|
+
groups.set(g, list);
|
|
290
|
+
});
|
|
291
|
+
const mintKey = (hit) => {
|
|
292
|
+
if (hit === undefined)
|
|
293
|
+
return undefined;
|
|
294
|
+
const list = groups.get(hit.group) ?? [];
|
|
295
|
+
const occurrenceIdx = list[hit.dupIdx];
|
|
296
|
+
return {
|
|
297
|
+
key: `${hit.group}${OCCURRENCE_KEY_SEP}${hit.dupIdx}`,
|
|
298
|
+
groupCount: list.length,
|
|
299
|
+
fp: occurrenceIdx === undefined ? "" : fingerprintToolResultOccurrence(messages[occurrenceIdx]),
|
|
300
|
+
...(occurrenceIdx === undefined ? {} : { occurrence: messages[occurrenceIdx] }),
|
|
301
|
+
};
|
|
302
|
+
};
|
|
303
|
+
const index = {
|
|
304
|
+
keyAt: (i) => mintKey(byIndex.get(i)),
|
|
305
|
+
keyOf: (m) => {
|
|
306
|
+
const direct = byObject.get(m);
|
|
307
|
+
if (direct !== undefined)
|
|
308
|
+
return mintKey(direct);
|
|
309
|
+
if (!isToolResult(m))
|
|
310
|
+
return undefined;
|
|
311
|
+
const g = occurrenceGroupId(m);
|
|
312
|
+
const list = groups.get(g);
|
|
313
|
+
return list !== undefined && list.length === 1 ? mintKey({ group: g, dupIdx: 0 }) : undefined;
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
if (ledger.entries.size === 0) {
|
|
317
|
+
return { messages, index };
|
|
318
|
+
}
|
|
319
|
+
let out;
|
|
320
|
+
const rekeyed = new Map();
|
|
321
|
+
for (const [key, entry] of ledger.entries) {
|
|
322
|
+
const sep = key.lastIndexOf(OCCURRENCE_KEY_SEP);
|
|
323
|
+
const group = key.slice(0, sep);
|
|
324
|
+
const dupIdx = Number(key.slice(sep + 1));
|
|
325
|
+
const list = groups.get(group);
|
|
326
|
+
if (list === undefined)
|
|
327
|
+
continue;
|
|
328
|
+
const m = list.length;
|
|
329
|
+
const shrink = Math.max(0, entry.groupCount - m);
|
|
330
|
+
const mapped = dupIdx - shrink;
|
|
331
|
+
if (mapped < 0 || mapped >= m)
|
|
332
|
+
continue;
|
|
333
|
+
const targetIdx = list[mapped];
|
|
334
|
+
const target = messages[targetIdx];
|
|
335
|
+
if (soleMarkerText(target) !== entry.marker) {
|
|
336
|
+
if (fingerprintToolResultOccurrence(target) !== entry.fp)
|
|
337
|
+
continue;
|
|
338
|
+
out = out ?? messages.slice();
|
|
339
|
+
out[targetIdx] = { ...target, content: [{ type: "text", text: entry.marker }] };
|
|
340
|
+
}
|
|
341
|
+
rekeyed.set(`${group}${OCCURRENCE_KEY_SEP}${mapped}`, { marker: entry.marker, groupCount: m, fp: entry.fp });
|
|
342
|
+
}
|
|
343
|
+
ledger.entries.clear();
|
|
344
|
+
for (const [k, v] of rekeyed)
|
|
345
|
+
ledger.entries.set(k, v);
|
|
346
|
+
return { messages: out ?? messages, index };
|
|
347
|
+
}
|
|
@@ -68,15 +68,42 @@ export type RuleSyncDropReason = keyof typeof RULE_SYNC_DROP_CODES;
|
|
|
68
68
|
* fence arm / local screening). `own_actor_forged` is inbound-only by construction. */
|
|
69
69
|
export type RuleQuarantineReason = Exclude<RuleSyncDropReason, "own_actor_forged">;
|
|
70
70
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
71
|
+
* WHO a notice code is for. `"user"` = a session-scoped disclosure the end user of that session is
|
|
72
|
+
* entitled to see (safe to project onto that session's event stream); `"operator"` = a
|
|
73
|
+
* deployment/config/ops fact for whoever runs the process.
|
|
74
|
+
*
|
|
75
|
+
* Two values, deliberately. "Can this notice be routed to a session" is NOT an audience value — it
|
|
76
|
+
* is the presence of `EngineNotice.sessionId`, a per-EMISSION fact. A third value would fold a
|
|
77
|
+
* routing property into an entitlement vocabulary, and the two answer different questions: several
|
|
78
|
+
* codes below carry a session attribution while the person entitled to the fact is still the
|
|
79
|
+
* operator (a failed offload write, a routing fallback, a consolidation recommendation).
|
|
80
|
+
*/
|
|
81
|
+
export type NoticeAudience = "user" | "operator";
|
|
82
|
+
/**
|
|
83
|
+
* The CLOSED catalog of EngineNotice codes this engine mints — the enumeration half of the
|
|
84
|
+
* presentation registry below, and the thing a consumer's own audience table can be diffed against
|
|
85
|
+
* (that is the point of exporting it: a downstream table with a row missing, or a row for a code
|
|
86
|
+
* this engine no longer mints, is a mechanically detectable disagreement rather than an argument).
|
|
87
|
+
*
|
|
88
|
+
* Kept in LOCKSTEP with {@link NOTICE_AUDIENCE} by the compiler (`satisfies Record<EngineNoticeCode,
|
|
89
|
+
* …>` below: a catalog entry with no audience row, or an audience row for a non-member, is a tsc
|
|
90
|
+
* error — a mirrored enumeration that is only checked at runtime is a mirror that drifts). The
|
|
91
|
+
* other half — "is the catalog itself still complete?" — cannot be a type: `EngineNotice.code` is
|
|
92
|
+
* declared `string` (a host may forward its own), so the gate test (governance-codes.test.ts) scans
|
|
93
|
+
* src/ for notice mint shapes and names any code that is minted but unregistered, or registered but
|
|
94
|
+
* no longer minted.
|
|
95
|
+
*/
|
|
96
|
+
export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.read_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "delegation.transcript_integrity", "mcp.revocation_probe_failed", "workflow.governance_key_stripped", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "route.fallback_to_primary", "route.base_url_changed_key_unchanged", "task.user_steer_undrained", "task.user_followup_undrained", "task.injection_priority_unimplemented", "tool_result.offload_put_failed"];
|
|
97
|
+
/** A code this engine mints (see {@link ENGINE_NOTICE_CODES}). NOT the type of
|
|
98
|
+
* `EngineNotice.code`, which stays `string` — a host forwarding its own notices through the same
|
|
99
|
+
* sink is a supported shape, and narrowing that field would break it. */
|
|
100
|
+
export type EngineNoticeCode = (typeof ENGINE_NOTICE_CODES)[number];
|
|
101
|
+
/**
|
|
102
|
+
* The presentation-tier registry — see {@link NOTICE_AUDIENCE_TABLE} for the per-row judgment.
|
|
103
|
+
* The exported TYPE stays the open `Record<string, …>` on purpose: a consumer indexing it with a
|
|
104
|
+
* runtime string (a code off a wire, a host's own) must keep compiling. The lockstep with
|
|
105
|
+
* {@link ENGINE_NOTICE_CODES} is enforced on the table above, where narrowing costs nobody anything.
|
|
79
106
|
*/
|
|
80
|
-
export declare const NOTICE_AUDIENCE: Readonly<Record<string,
|
|
107
|
+
export declare const NOTICE_AUDIENCE: Readonly<Record<string, NoticeAudience>>;
|
|
81
108
|
/** The audience for `code` — table lookup with the conservative `"operator"` default. */
|
|
82
|
-
export declare function noticeAudienceOf(code: string):
|
|
109
|
+
export declare function noticeAudienceOf(code: string): NoticeAudience;
|
|
@@ -17,6 +17,7 @@ export const NON_GOVERNANCE_MEMORY_CODES = new Set([
|
|
|
17
17
|
"memory.session_polluted",
|
|
18
18
|
"memory.harvest_quarantined",
|
|
19
19
|
"memory.delegation_static_mark_waived",
|
|
20
|
+
"memory.content_class_declared",
|
|
20
21
|
"memory.consolidation_driver_superseded",
|
|
21
22
|
"memory.challenge_sweep_failed",
|
|
22
23
|
"memory.lineage_settle_failed",
|
|
@@ -84,14 +85,69 @@ export const RULE_SYNC_DROP_CODES = {
|
|
|
84
85
|
below_gc_frontier: "local-quarantined",
|
|
85
86
|
server_rejected: "local-quarantined",
|
|
86
87
|
};
|
|
87
|
-
export const
|
|
88
|
+
export const ENGINE_NOTICE_CODES = [
|
|
89
|
+
"config.autocompact_window_clamped",
|
|
90
|
+
"config.env_timeout_discarded",
|
|
91
|
+
"config.materialize_env_discarded",
|
|
92
|
+
"config.models_swapped",
|
|
93
|
+
"config.read_face_deployment_clamped",
|
|
94
|
+
"config.tool_model_gate_removed",
|
|
95
|
+
"config.tool_model_gate_unknown_class",
|
|
96
|
+
"config.tool_model_gate_env_invalid",
|
|
97
|
+
"delegation.transcript_integrity",
|
|
98
|
+
"mcp.revocation_probe_failed",
|
|
99
|
+
"workflow.governance_key_stripped",
|
|
100
|
+
"memory.session_polluted",
|
|
101
|
+
"memory.harvest_quarantined",
|
|
102
|
+
"memory.delegation_static_mark_waived",
|
|
103
|
+
"memory.content_class_declared",
|
|
104
|
+
"memory.hold_opened",
|
|
105
|
+
"memory.hold_released",
|
|
106
|
+
"memory.hold_disposed",
|
|
107
|
+
"memory.consolidation_recommended",
|
|
108
|
+
"memory.consolidation_committed",
|
|
109
|
+
"memory.consolidation_conflict",
|
|
110
|
+
"memory.consolidation_incomplete",
|
|
111
|
+
"memory.consolidation_refused",
|
|
112
|
+
"route.fallback_to_primary",
|
|
113
|
+
"route.base_url_changed_key_unchanged",
|
|
114
|
+
"task.user_steer_undrained",
|
|
115
|
+
"task.user_followup_undrained",
|
|
116
|
+
"task.injection_priority_unimplemented",
|
|
117
|
+
"tool_result.offload_put_failed",
|
|
118
|
+
];
|
|
119
|
+
const NOTICE_AUDIENCE_TABLE = {
|
|
88
120
|
"memory.session_polluted": "user",
|
|
89
121
|
"memory.harvest_quarantined": "user",
|
|
90
122
|
"memory.delegation_static_mark_waived": "user",
|
|
91
123
|
"memory.hold_opened": "user",
|
|
92
124
|
"memory.hold_released": "user",
|
|
93
125
|
"memory.hold_disposed": "user",
|
|
126
|
+
"task.user_steer_undrained": "user",
|
|
127
|
+
"task.user_followup_undrained": "user",
|
|
128
|
+
"config.autocompact_window_clamped": "operator",
|
|
129
|
+
"config.env_timeout_discarded": "operator",
|
|
130
|
+
"config.materialize_env_discarded": "operator",
|
|
131
|
+
"config.models_swapped": "operator",
|
|
132
|
+
"config.read_face_deployment_clamped": "operator",
|
|
133
|
+
"config.tool_model_gate_removed": "operator",
|
|
134
|
+
"config.tool_model_gate_unknown_class": "operator",
|
|
135
|
+
"config.tool_model_gate_env_invalid": "operator",
|
|
136
|
+
"delegation.transcript_integrity": "operator",
|
|
137
|
+
"mcp.revocation_probe_failed": "operator",
|
|
138
|
+
"workflow.governance_key_stripped": "operator",
|
|
139
|
+
"memory.content_class_declared": "operator",
|
|
140
|
+
"memory.consolidation_recommended": "operator",
|
|
141
|
+
"memory.consolidation_committed": "operator",
|
|
142
|
+
"memory.consolidation_conflict": "operator",
|
|
143
|
+
"memory.consolidation_incomplete": "operator",
|
|
144
|
+
"memory.consolidation_refused": "operator",
|
|
145
|
+
"route.fallback_to_primary": "operator",
|
|
146
|
+
"route.base_url_changed_key_unchanged": "operator",
|
|
147
|
+
"task.injection_priority_unimplemented": "operator",
|
|
148
|
+
"tool_result.offload_put_failed": "operator",
|
|
94
149
|
};
|
|
150
|
+
export const NOTICE_AUDIENCE = NOTICE_AUDIENCE_TABLE;
|
|
95
151
|
export function noticeAudienceOf(code) {
|
|
96
152
|
return NOTICE_AUDIENCE[code] ?? "operator";
|
|
97
153
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RunnerDeps, TaskSpec } from "./types.js";
|
|
1
|
+
import type { McpServerSpec, RunnerDeps, TaskSpec } from "./types.js";
|
|
2
2
|
import type { ToolPolicy } from "./tool-policy.js";
|
|
3
3
|
/** The closed set of administratively lockable configuration keys. Adding a member is a deliberate
|
|
4
4
|
* edit HERE (tsc forces the registry row), never a free-form string. */
|
|
@@ -38,9 +38,12 @@ export interface LockedConfig {
|
|
|
38
38
|
export interface LockedPreflight {
|
|
39
39
|
/** The validated lock set (empty when the deployment declares none). */
|
|
40
40
|
lockedKeys: ReadonlySet<LockedKey>;
|
|
41
|
-
/** Effective MCP server list for this task: `spec.mcp` when the
|
|
42
|
-
*
|
|
43
|
-
* no
|
|
41
|
+
/** Effective MCP server list for this task: a validated, frozen SNAPSHOT of `spec.mcp` when the
|
|
42
|
+
* key is unlocked (see {@link snapshotAndValidateMcpEntries} — the content-class declaration is read once
|
|
43
|
+
* and judged here, so no mint site downstream can be handed a different value than the door
|
|
44
|
+
* approved); with `mcp` locked a spec-supplied value has already been refused, so this is always
|
|
45
|
+
* `undefined` (core has no deployment-level MCP seat — a locked deployment mounts no
|
|
46
|
+
* task-supplied servers). */
|
|
44
47
|
mcp: TaskSpec["mcp"];
|
|
45
48
|
/** Effective caller tool policy: the same `spec.toolPolicy ?? deps.toolPolicy` slot the gate has
|
|
46
49
|
* always enforced, resolved ONCE here. With `toolPolicy` locked, the deployment's own
|
|
@@ -61,6 +64,35 @@ export interface LockedPreflight {
|
|
|
61
64
|
* then fails the first prepare instead of silently guarding nothing).
|
|
62
65
|
*/
|
|
63
66
|
export declare function resolveLockedKeys(config: LockedConfig | undefined): ReadonlySet<LockedKey>;
|
|
67
|
+
/**
|
|
68
|
+
* design/378 — turn a caller-owned MCP entry list into FROZEN PLAIN DATA, reading every own property
|
|
69
|
+
* exactly once.
|
|
70
|
+
*
|
|
71
|
+
* WHY A COPY. `McpServerSpec.contentOrigin` decides whether a server's tools mark the session's
|
|
72
|
+
* memory, and the entry objects travel a long way: the preparation door validates them, then
|
|
73
|
+
* `materializeMcpTools` connects them, then every mid-task refresh re-reads the same object. A value
|
|
74
|
+
* read off a LIVE object is only true for as long as the object holds still, and the fail-open
|
|
75
|
+
* direction is the reachable one — so both boundaries consume data instead.
|
|
76
|
+
*
|
|
77
|
+
* The read discipline is the load-bearing part, and it is why this is a spread rather than
|
|
78
|
+
* "read the property, then copy the object": a getter answering ALTERNATING LEGAL values would
|
|
79
|
+
* satisfy any number of re-reads (`"external"` to a validator, `"local"` to a mint site), and no
|
|
80
|
+
* vocabulary guard anywhere has purchase on a legal value. One spread evaluates every own property
|
|
81
|
+
* EXACTLY ONCE and materializes the result as plain data; every later reader — validator, connect,
|
|
82
|
+
* refresh — then sees the same byte. Frozen so nothing downstream rewrites it.
|
|
83
|
+
*
|
|
84
|
+
* Boundary, stated: the copy is one level deep — nested option objects (transport, headers, env,
|
|
85
|
+
* toolAxes) stay shared with the caller's entry exactly as before, because the axis this owns is the
|
|
86
|
+
* content class; readers of the nested shapes normalize their own values (the transport DISCRIMINATOR
|
|
87
|
+
* is derived, never echoed raw). Own ENUMERABLE properties only: an entry whose fields live on a
|
|
88
|
+
* prototype was never the plain configuration record this type describes, and its absent class reads
|
|
89
|
+
* fail-closed like any other absence.
|
|
90
|
+
*
|
|
91
|
+
* Exported because the SECOND boundary needs the identical guarantee: `materializeMcpTools` is public
|
|
92
|
+
* and a library caller reaches it without passing any door, so it snapshots its own input through
|
|
93
|
+
* this same function (idempotent over an already-frozen entry). One spelling, both entrances.
|
|
94
|
+
*/
|
|
95
|
+
export declare function snapshotMcpEntries(entries: readonly McpServerSpec[]): McpServerSpec[];
|
|
64
96
|
/**
|
|
65
97
|
* The single preflight resolver: validate the lock declaration, refuse a spec that tries to occupy
|
|
66
98
|
* a locked slot (`config.locked_key`, two-state — the whole prepare is rejected, nothing is
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isToolContentOrigin, TOOL_CONTENT_ORIGINS } from "./memory-engine/content-origin.js";
|
|
2
|
+
import { inlineUntrusted } from "./untrusted-text.js";
|
|
1
3
|
export const LOCKED_KEY_REGISTRY = {
|
|
2
4
|
mcp: { specFields: ["mcp"] },
|
|
3
5
|
toolPolicy: { specFields: ["toolPolicy", "basePolicyForResumeEdit"] },
|
|
@@ -23,6 +25,37 @@ export function resolveLockedKeys(config) {
|
|
|
23
25
|
}
|
|
24
26
|
return keys;
|
|
25
27
|
}
|
|
28
|
+
export function snapshotMcpEntries(entries) {
|
|
29
|
+
return entries.map((entry) => Object.freeze({ ...entry }));
|
|
30
|
+
}
|
|
31
|
+
function describeDeclared(value) {
|
|
32
|
+
try {
|
|
33
|
+
return JSON.stringify(value) ?? String(value);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
try {
|
|
37
|
+
return String(value);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return `[unrepresentable ${typeof value}]`;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function snapshotAndValidateMcpEntries(entries) {
|
|
45
|
+
const copies = snapshotMcpEntries(entries);
|
|
46
|
+
for (const copy of copies) {
|
|
47
|
+
const declared = copy.contentOrigin;
|
|
48
|
+
if (declared !== undefined && !isToolContentOrigin(declared)) {
|
|
49
|
+
throw codedError("config.mcp_content_class", `MCP server ${inlineUntrusted(typeof copy.name === "string" ? JSON.stringify(copy.name) : describeDeclared(copy.name), 80)} declares contentOrigin ` +
|
|
50
|
+
`${inlineUntrusted(describeDeclared(declared), 80)}, which is not one of ` +
|
|
51
|
+
`${TOOL_CONTENT_ORIGINS.map((v) => `"${v}"`).join(" | ")}. The declaration decides whether this server's tools ` +
|
|
52
|
+
`mark the session's memory externally exposed — a value this engine cannot read is refused at the door rather ` +
|
|
53
|
+
`than folded to a class, because folding it would let a deployment believe a declaration is in force while every ` +
|
|
54
|
+
`call keeps writing a one-way durable mark.`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return copies;
|
|
58
|
+
}
|
|
26
59
|
export function preflightLockedConfig(spec, deps) {
|
|
27
60
|
const lockedKeys = resolveLockedKeys(deps.lockedConfig);
|
|
28
61
|
for (const key of lockedKeys) {
|
|
@@ -36,7 +69,7 @@ export function preflightLockedConfig(spec, deps) {
|
|
|
36
69
|
}
|
|
37
70
|
return {
|
|
38
71
|
lockedKeys,
|
|
39
|
-
mcp: lockedKeys.has("mcp") ? undefined : spec.mcp,
|
|
72
|
+
mcp: lockedKeys.has("mcp") || spec.mcp === undefined ? undefined : snapshotAndValidateMcpEntries(spec.mcp),
|
|
40
73
|
toolPolicy: lockedKeys.has("toolPolicy") ? deps.toolPolicy : (spec.toolPolicy ?? deps.toolPolicy),
|
|
41
74
|
basePolicyForResumeEdit: lockedKeys.has("toolPolicy") ? deps.basePolicyForResumeEdit : (spec.basePolicyForResumeEdit ?? deps.basePolicyForResumeEdit),
|
|
42
75
|
};
|
package/dist/core/mcp.js
CHANGED
|
@@ -14,6 +14,7 @@ import { truncateError } from "./tool-errors.js";
|
|
|
14
14
|
import { delimitUntrusted, inlineUntrusted, sanitizeUntrustedText } from "./untrusted-text.js";
|
|
15
15
|
import { discloseReminderShaped, observeReminderMarkEcho } from "./reminder-disclosure.js";
|
|
16
16
|
import { withContentOrigin } from "./memory-engine/content-origin.js";
|
|
17
|
+
import { snapshotMcpEntries } from "./locked-config.js";
|
|
17
18
|
import { validateJsonSchemaShape } from "./runner/strict-output-schema.js";
|
|
18
19
|
export const MCP_PREFIX = MCP_NAMESPACE.prefix;
|
|
19
20
|
const MCP_OUTPUT_TOKENS_DEFAULT = 25_000;
|
|
@@ -753,8 +754,9 @@ export function mcpToolSchemaProblem(schema) {
|
|
|
753
754
|
return undefined;
|
|
754
755
|
}
|
|
755
756
|
export async function materializeMcpTools(specs, principal, onElicit, imageResizer, reminderDisclosure, mcpRevocations) {
|
|
757
|
+
const entries = snapshotMcpEntries(specs);
|
|
756
758
|
{
|
|
757
|
-
const collision = findNamespacePrefixCollision(MCP_NAMESPACE,
|
|
759
|
+
const collision = findNamespacePrefixCollision(MCP_NAMESPACE, entries.map((s) => s.name));
|
|
758
760
|
if (collision) {
|
|
759
761
|
const [a, b] = collision.peers;
|
|
760
762
|
const e = new Error(a === b
|
|
@@ -801,11 +803,11 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
|
|
|
801
803
|
const droppedTools = [];
|
|
802
804
|
let disposing = false;
|
|
803
805
|
const serverHandles = [];
|
|
804
|
-
const settled = await Promise.allSettled(
|
|
806
|
+
const settled = await Promise.allSettled(entries.map((spec) => connectServer(spec, principal, onElicit, imageResizer, mcpDisclosure, isServerRevoked)));
|
|
805
807
|
try {
|
|
806
|
-
for (let i = 0; i <
|
|
808
|
+
for (let i = 0; i < entries.length; i++) {
|
|
807
809
|
const r = settled[i];
|
|
808
|
-
const spec =
|
|
810
|
+
const spec = entries[i];
|
|
809
811
|
if (r.status === "fulfilled") {
|
|
810
812
|
const s = r.value;
|
|
811
813
|
clients.push(s.client);
|
|
@@ -1513,6 +1515,7 @@ async function connectServer(spec, principal, onElicit, imageResizer, reminderDi
|
|
|
1513
1515
|
}
|
|
1514
1516
|
}
|
|
1515
1517
|
function intakeListedTools(listed, spec, client, health, imageResizer, reminderDisclosure, isServerRevoked) {
|
|
1518
|
+
const declaredContentOrigin = spec.contentOrigin;
|
|
1516
1519
|
const serverTools = [];
|
|
1517
1520
|
const serverAxes = [];
|
|
1518
1521
|
const dropped = [];
|
|
@@ -1576,7 +1579,7 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
|
|
|
1576
1579
|
const mcpToolMeta = t._meta;
|
|
1577
1580
|
const mcpMaxResultSizeChars = resolveMcpDeclaredResultSize(mcpToolMeta);
|
|
1578
1581
|
const mcpAlwaysLoad = mcpToolMeta?.["anthropic/alwaysLoad"] === true;
|
|
1579
|
-
|
|
1582
|
+
const mounted = {
|
|
1580
1583
|
name: namespacedName,
|
|
1581
1584
|
description: capMcpToolDescription(effectiveDescription ?? `MCP tool ${inlineUntrusted(sanitizeMcpModelFacingText(remoteName))} from ${spec.name}`),
|
|
1582
1585
|
label: `${spec.name}:${remoteName}`,
|
|
@@ -1678,7 +1681,8 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
|
|
|
1678
1681
|
return { content, details: { type: "mcp", structuredContent: sc }, terminate: false };
|
|
1679
1682
|
return { content, details: res, terminate: false };
|
|
1680
1683
|
},
|
|
1681
|
-
}
|
|
1684
|
+
};
|
|
1685
|
+
serverTools.push(declaredContentOrigin !== undefined ? withContentOrigin(mounted, declaredContentOrigin) : mounted);
|
|
1682
1686
|
}
|
|
1683
1687
|
return { serverTools, serverAxes, dropped, advisories };
|
|
1684
1688
|
}
|