@sema-agent/core 5.59.0 → 5.60.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -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/stream-engine.js +9 -1
- package/dist/core/auto-compaction.js +2 -2
- package/dist/core/checkpoint-store.d.ts +172 -25
- 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 +1 -1
- package/dist/core/governance-codes.js +5 -0
- package/dist/core/locked-config.d.ts +36 -4
- package/dist/core/locked-config.js +34 -1
- package/dist/core/mcp.d.ts +4 -5
- package/dist/core/mcp.js +10 -6
- 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 +5 -6
- 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/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 +107 -8
- package/dist/core/runner/runtask.js +97 -9
- package/dist/core/store-contracts/checkpoint-store-contract.js +32 -0
- package/dist/core/tool-policy.d.ts +32 -10
- package/dist/core/tool-policy.js +3 -3
- package/dist/core/tools.js +1 -1
- package/dist/core/trace.d.ts +36 -0
- package/dist/core/types.d.ts +127 -2
- 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 +1 -1
- package/dist/index.js +1 -1
- package/dist/tools/fs/fs-bash.js +1 -2
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +5 -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
|
+
}
|
|
@@ -93,7 +93,7 @@ export type NoticeAudience = "user" | "operator";
|
|
|
93
93
|
* src/ for notice mint shapes and names any code that is minted but unregistered, or registered but
|
|
94
94
|
* no longer minted.
|
|
95
95
|
*/
|
|
96
|
-
export declare const ENGINE_NOTICE_CODES: readonly ["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.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"];
|
|
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
97
|
/** A code this engine mints (see {@link ENGINE_NOTICE_CODES}). NOT the type of
|
|
98
98
|
* `EngineNotice.code`, which stays `string` — a host forwarding its own notices through the same
|
|
99
99
|
* sink is a supported shape, and narrowing that field would break it. */
|
|
@@ -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",
|
|
@@ -85,6 +86,7 @@ export const RULE_SYNC_DROP_CODES = {
|
|
|
85
86
|
server_rejected: "local-quarantined",
|
|
86
87
|
};
|
|
87
88
|
export const ENGINE_NOTICE_CODES = [
|
|
89
|
+
"config.autocompact_window_clamped",
|
|
88
90
|
"config.env_timeout_discarded",
|
|
89
91
|
"config.materialize_env_discarded",
|
|
90
92
|
"config.models_swapped",
|
|
@@ -98,6 +100,7 @@ export const ENGINE_NOTICE_CODES = [
|
|
|
98
100
|
"memory.session_polluted",
|
|
99
101
|
"memory.harvest_quarantined",
|
|
100
102
|
"memory.delegation_static_mark_waived",
|
|
103
|
+
"memory.content_class_declared",
|
|
101
104
|
"memory.hold_opened",
|
|
102
105
|
"memory.hold_released",
|
|
103
106
|
"memory.hold_disposed",
|
|
@@ -122,6 +125,7 @@ const NOTICE_AUDIENCE_TABLE = {
|
|
|
122
125
|
"memory.hold_disposed": "user",
|
|
123
126
|
"task.user_steer_undrained": "user",
|
|
124
127
|
"task.user_followup_undrained": "user",
|
|
128
|
+
"config.autocompact_window_clamped": "operator",
|
|
125
129
|
"config.env_timeout_discarded": "operator",
|
|
126
130
|
"config.materialize_env_discarded": "operator",
|
|
127
131
|
"config.models_swapped": "operator",
|
|
@@ -132,6 +136,7 @@ const NOTICE_AUDIENCE_TABLE = {
|
|
|
132
136
|
"delegation.transcript_integrity": "operator",
|
|
133
137
|
"mcp.revocation_probe_failed": "operator",
|
|
134
138
|
"workflow.governance_key_stripped": "operator",
|
|
139
|
+
"memory.content_class_declared": "operator",
|
|
135
140
|
"memory.consolidation_recommended": "operator",
|
|
136
141
|
"memory.consolidation_committed": "operator",
|
|
137
142
|
"memory.consolidation_conflict": "operator",
|
|
@@ -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.d.ts
CHANGED
|
@@ -177,11 +177,10 @@ export interface MaterializedMcp {
|
|
|
177
177
|
* fails the WHOLE model request (provider 400), bricking the task for every healthy tool
|
|
178
178
|
* (CC 2.1.216 ships the same intake prune, `tengu_mcp_drop_invalid_tool_schemas`).
|
|
179
179
|
*
|
|
180
|
-
* HONEST REACH (pinned in mcp-dropped-tools.test.ts):
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
* transport/SDK revision delivers schemas the client does not fully validate.
|
|
180
|
+
* HONEST REACH (pinned in mcp-dropped-tools.test.ts): listings arrive through the lenient reader
|
|
181
|
+
* (`listToolsLenient` — raw request, no SDK-client zod pass), so a string-root or otherwise odd
|
|
182
|
+
* schema DOES reach this gate and is judged per tool; healthy siblings survive. This per-tool gate
|
|
183
|
+
* is therefore the PRIMARY adjudicator for schema shape, not a fallback behind SDK validation.
|
|
185
184
|
*
|
|
186
185
|
* The drop is NEVER silent, two faces (CC parity — its `mcp_dropped_tools_delta` system reminder):
|
|
187
186
|
* - operator: prepare-task forwards each entry to `onError(phase:"mcp")` alongside `warnings`;
|
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
|
}
|
|
@@ -22,13 +22,22 @@
|
|
|
22
22
|
* does not, the default is EXTERNAL (fail-closed: unknown = external), overridable only through the
|
|
23
23
|
* explicit `TaskSpec.memory.trustedTools` allowlist. A core-mounted built-in without a declaration
|
|
24
24
|
* defaults local: core owns those definitions, and the fail-closed default exists for tools core
|
|
25
|
-
* CANNOT vouch for, not for its own.
|
|
25
|
+
* CANNOT vouch for, not for its own. A declaration is honored only when it SPELLS a member of the
|
|
26
|
+
* closed vocabulary — an unreadable one is not a declaration.
|
|
27
|
+
*
|
|
28
|
+
* The three tier names are BOUNDARY-RELATIVE, not statements about topology: `"local"` means "does
|
|
29
|
+
* not bring content from outside the deployment's trust boundary", which a unix socket to another
|
|
30
|
+
* process the deployment itself runs satisfies and a loopback HTTP call to somebody else's service
|
|
31
|
+
* does not. Distance is not the axis; whose content it is, is.
|
|
26
32
|
*
|
|
27
33
|
* There is deliberately no "gate inactive" state: every mounted tool gets a class.
|
|
28
34
|
*/
|
|
29
35
|
import type { ToolContentOrigin } from "../types.js";
|
|
30
36
|
export interface ClassifyToolContentOriginInput {
|
|
31
|
-
/** The tool's own `contentOrigin` declaration (ToolSpec/AgentTool carry) — wins when present
|
|
37
|
+
/** The tool's own `contentOrigin` declaration (ToolSpec/AgentTool carry) — wins when present AND
|
|
38
|
+
* a member of the closed vocabulary ({@link isToolContentOrigin}); a present non-member is the
|
|
39
|
+
* unreadable-declaration case and classifies EXTERNAL. The annotation is intent, not a guarantee:
|
|
40
|
+
* the property rides tool objects untyped. */
|
|
32
41
|
declared?: ToolContentOrigin;
|
|
33
42
|
/** True ⇔ the name belongs to a protocol namespace (mcp__/a2a__ …) — an external channel. */
|
|
34
43
|
isProtocolTool: boolean;
|
|
@@ -37,6 +46,10 @@ export interface ClassifyToolContentOriginInput {
|
|
|
37
46
|
/** True ⇔ `TaskSpec.memory.trustedTools` names this tool (explicit host exemption). */
|
|
38
47
|
trusted: boolean;
|
|
39
48
|
}
|
|
49
|
+
/** The vocabulary in wire/message order — the one spelling list a refusal quotes back. */
|
|
50
|
+
export declare const TOOL_CONTENT_ORIGINS: readonly ToolContentOrigin[];
|
|
51
|
+
/** Runtime membership in the closed vocabulary. */
|
|
52
|
+
export declare function isToolContentOrigin(value: unknown): value is ToolContentOrigin;
|
|
40
53
|
export declare function classifyToolContentOrigin(input: ClassifyToolContentOriginInput): ToolContentOrigin;
|
|
41
54
|
/**
|
|
42
55
|
* Declare `origin` on a tool object built OUTSIDE `defineTool` — a raw AgentTool literal, which is
|
|
@@ -60,6 +73,15 @@ export interface AgentToolFace {
|
|
|
60
73
|
export interface AgentPoolTool {
|
|
61
74
|
name: string;
|
|
62
75
|
aliases?: readonly string[];
|
|
76
|
+
/** The pool row's own declaration — a STATIC surface the deployment writes when it composes the
|
|
77
|
+
* delegation tool, and the only class information this judgment has about the child's roster.
|
|
78
|
+
*
|
|
79
|
+
* A per-entry MCP declaration (design/378, `McpServerSpec.contentOrigin`) does NOT reach here: the
|
|
80
|
+
* pool is a declaration, not a live roster, and this judgment never sees the server entries. A
|
|
81
|
+
* deployment that hands a declared server's `mcp__` tools to children MIRRORS the value onto the
|
|
82
|
+
* matching pool rows; not mirroring it leaves them on the protocol arm, so the delegation reads
|
|
83
|
+
* external and the parent over-marks — the safe direction, and the reason this is a documented
|
|
84
|
+
* duty rather than a refusal. */
|
|
63
85
|
contentOrigin?: ToolContentOrigin;
|
|
64
86
|
}
|
|
65
87
|
/**
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
const TOOL_CONTENT_ORIGIN_SET = { external: true, execution: true, local: true };
|
|
2
|
+
export const TOOL_CONTENT_ORIGINS = Object.keys(TOOL_CONTENT_ORIGIN_SET);
|
|
3
|
+
export function isToolContentOrigin(value) {
|
|
4
|
+
return typeof value === "string" && Object.prototype.hasOwnProperty.call(TOOL_CONTENT_ORIGIN_SET, value);
|
|
5
|
+
}
|
|
1
6
|
export function classifyToolContentOrigin(input) {
|
|
2
7
|
if (input.declared !== undefined)
|
|
3
|
-
return input.declared;
|
|
8
|
+
return isToolContentOrigin(input.declared) ? input.declared : "external";
|
|
4
9
|
if (input.trusted)
|
|
5
10
|
return "local";
|
|
6
11
|
if (input.isProtocolTool)
|