@sema-agent/core 5.54.0 → 5.55.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 +94 -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/verify.d.ts +27 -3
- package/dist/agents/verify.js +7 -2
- package/dist/core/governance-codes.js +14 -0
- package/dist/core/hooks.js +1 -1
- 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 +22 -0
- package/dist/core/permission-rules.js +60 -6
- 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 +31 -1
- package/dist/core/runner/prepare-task.js +31 -14
- package/dist/core/runner/runtask.d.ts +8 -1
- package/dist/core/runner/runtask.js +12 -10
- 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/tool-policy.d.ts +18 -0
- package/dist/core/tool-policy.js +19 -8
- package/dist/core/types.d.ts +89 -6
- package/dist/core/untrusted-egress.js +12 -2
- package/dist/core/untrusted-text.d.ts +189 -3
- package/dist/core/untrusted-text.js +416 -6
- package/dist/engine/loop/types.d.ts +7 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/prompts/default.d.ts +12 -2
- package/dist/tools/fs/index.d.ts +3 -1
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +28 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PRESENT_PLAN_TOOL_NAME } from "../present-plan-tool.js";
|
|
2
|
-
import {
|
|
2
|
+
import { delimitUntrustedWithClip, sanitizeUntrustedText, SHELLED_BODY_ENVELOPE_TAGS } from "../untrusted-text.js";
|
|
3
3
|
import { protocolOf } from "../protocol-table.js";
|
|
4
4
|
import { buildSkillsBlock, skillListingLine } from "./synthetic-tools.js";
|
|
5
5
|
import { TOOL_SEARCH_NAME as TOOL_SEARCH_TOOL_NAME } from "./tool-disclosure.js";
|
|
@@ -20,6 +20,11 @@ export const CHANGED_FILES_MTIME_EPS_MS = 2000;
|
|
|
20
20
|
export const ATTACHMENT_BYTE_CAP = 8 * 1024;
|
|
21
21
|
const PROJECTION_ITEMS_MAX = 50;
|
|
22
22
|
const PROJECTION_CONTENT_MAX = 80;
|
|
23
|
+
const ATTACHMENT_TAGS_DEFAULT = [...SHELLED_BODY_ENVELOPE_TAGS];
|
|
24
|
+
const ATTACHMENT_TAGS_SKILLS_OWNER = SHELLED_BODY_ENVELOPE_TAGS.filter((t) => t !== "skills");
|
|
25
|
+
export function attachmentEnvelopeTags(source) {
|
|
26
|
+
return source === "skills_listing" ? ATTACHMENT_TAGS_SKILLS_OWNER : ATTACHMENT_TAGS_DEFAULT;
|
|
27
|
+
}
|
|
23
28
|
export const INSTRUCTIONS_CHANGE_BYTE_CAP = 512;
|
|
24
29
|
export function createAttachmentState() {
|
|
25
30
|
return {
|
|
@@ -358,8 +363,9 @@ export function renderBudgetUsd(used, total) {
|
|
|
358
363
|
return `USD budget: $${used}/$${total}; $${total - used} remaining`;
|
|
359
364
|
}
|
|
360
365
|
export function renderOrphanedBackgroundTasks(tasks) {
|
|
366
|
+
const safe = (t) => sanitizeUntrustedText(t, SHELLED_BODY_ENVELOPE_TAGS);
|
|
361
367
|
return (`The container was restarted. The following background tasks were running and are now stopped:\n` +
|
|
362
|
-
tasks.map((t) => `- ${t.description || "(no description)"} (task ${t.id})`).join("\n") +
|
|
368
|
+
tasks.map((t) => `- ${safe(t.description || "(no description)")} (task ${safe(t.id)})`).join("\n") +
|
|
363
369
|
`\nRe-create them if still needed.`);
|
|
364
370
|
}
|
|
365
371
|
function renderChangedFiles(paths) {
|
|
@@ -449,7 +455,7 @@ export const AGENT_TOOLS_NOTE_DEFAULT = "All tools";
|
|
|
449
455
|
export const AGENT_CONCURRENCY_NOTE = "When you launch multiple agents for independent work, send them in a single message with multiple tool uses so they run concurrently.";
|
|
450
456
|
export const AMBIENT_CONTEXT_NOTE = "This is ambient context — do not narrate it to the user unless they ask or it is directly relevant to their request.";
|
|
451
457
|
export function agentListingInitialHeader(toolName) {
|
|
452
|
-
return `Available agent types for the ${toolName} tool:`;
|
|
458
|
+
return `Available agent types for the ${sanitizeUntrustedText(toolName, SHELLED_BODY_ENVELOPE_TAGS)} tool:`;
|
|
453
459
|
}
|
|
454
460
|
export function replayAnnouncedListing(texts, headers) {
|
|
455
461
|
let announced;
|
|
@@ -492,13 +498,17 @@ export function replayAnnouncedListing(texts, headers) {
|
|
|
492
498
|
return announced;
|
|
493
499
|
}
|
|
494
500
|
export const AGENT_LISTING_REMOVED_HEADER = "The following agent types are no longer available:";
|
|
501
|
+
function listingIdentity(name) {
|
|
502
|
+
return sanitizeUntrustedText(name, SHELLED_BODY_ENVELOPE_TAGS);
|
|
503
|
+
}
|
|
495
504
|
export function agentListingDeltaHeader(toolName) {
|
|
496
|
-
return `New agent types are now available for the ${toolName} tool:`;
|
|
505
|
+
return `New agent types are now available for the ${sanitizeUntrustedText(toolName, SHELLED_BODY_ENVELOPE_TAGS)} tool:`;
|
|
497
506
|
}
|
|
498
507
|
export function renderAgentListingDelta(state, entries, toolName, models) {
|
|
508
|
+
const safe = (t) => sanitizeUntrustedText(t, SHELLED_BODY_ENVELOPE_TAGS);
|
|
499
509
|
const line = (e) => {
|
|
500
|
-
const base = e.description ? `- ${e.name}: ${e.description}` : `- ${e.name}`;
|
|
501
|
-
return `${base} (Tools: ${e.tools ?? AGENT_TOOLS_NOTE_DEFAULT})`;
|
|
510
|
+
const base = e.description ? `- ${safe(e.name)}: ${safe(e.description)}` : `- ${safe(e.name)}`;
|
|
511
|
+
return `${base} (Tools: ${safe(e.tools ?? AGENT_TOOLS_NOTE_DEFAULT)})`;
|
|
502
512
|
};
|
|
503
513
|
const announced = state.announcedAgentTypes;
|
|
504
514
|
if (announced === undefined) {
|
|
@@ -511,18 +521,22 @@ export function renderAgentListingDelta(state, entries, toolName, models) {
|
|
|
511
521
|
blocks.push(modelsAvailableLine(models));
|
|
512
522
|
return blocks.join("\n\n");
|
|
513
523
|
}
|
|
514
|
-
const
|
|
515
|
-
const
|
|
524
|
+
const announcedIds = new Set([...announced.keys()].map(listingIdentity));
|
|
525
|
+
const added = entries.filter((e) => !announcedIds.has(listingIdentity(e.name))).toSorted((a, b) => a.name.localeCompare(b.name));
|
|
526
|
+
const removed = [...announced.keys()]
|
|
527
|
+
.filter((n) => !entries.some((e) => listingIdentity(e.name) === listingIdentity(n)))
|
|
528
|
+
.sort();
|
|
516
529
|
const modelsDrifted = state.announcedModels !== undefined &&
|
|
517
530
|
models !== undefined &&
|
|
518
|
-
(state.announcedModels.length !== models.length ||
|
|
531
|
+
(state.announcedModels.length !== models.length ||
|
|
532
|
+
state.announcedModels.some((m, i) => listingIdentity(m) !== listingIdentity(models[i])));
|
|
519
533
|
if (added.length === 0 && removed.length === 0 && !modelsDrifted)
|
|
520
534
|
return undefined;
|
|
521
535
|
const blocks = [];
|
|
522
536
|
if (added.length > 0)
|
|
523
537
|
blocks.push(`${agentListingDeltaHeader(toolName)}\n${added.map(line).join("\n")}`);
|
|
524
538
|
if (removed.length > 0) {
|
|
525
|
-
blocks.push(`${AGENT_LISTING_REMOVED_HEADER}\n${removed.map((n) => `- ${n}`).join("\n")}`);
|
|
539
|
+
blocks.push(`${AGENT_LISTING_REMOVED_HEADER}\n${removed.map((n) => `- ${safe(n)}`).join("\n")}`);
|
|
526
540
|
blocks.push(AMBIENT_CONTEXT_NOTE);
|
|
527
541
|
}
|
|
528
542
|
if (modelsDrifted)
|
|
@@ -531,7 +545,8 @@ export function renderAgentListingDelta(state, entries, toolName, models) {
|
|
|
531
545
|
}
|
|
532
546
|
export const MODELS_AVAILABLE_PREFIX = "Models available for the 'model' parameter: ";
|
|
533
547
|
function modelsAvailableLine(models) {
|
|
534
|
-
|
|
548
|
+
const named = models.map((m) => sanitizeUntrustedText(m, SHELLED_BODY_ENVELOPE_TAGS));
|
|
549
|
+
return `${MODELS_AVAILABLE_PREFIX}${named.length > 0 ? named.join(", ") : "(none)"}`;
|
|
535
550
|
}
|
|
536
551
|
export function replayAnnouncedModels(texts) {
|
|
537
552
|
let last;
|
|
@@ -563,15 +578,16 @@ export function renderSkillsListingDelta(state, entries) {
|
|
|
563
578
|
return undefined;
|
|
564
579
|
return buildSkillsBlock(entries);
|
|
565
580
|
}
|
|
566
|
-
const
|
|
567
|
-
const
|
|
581
|
+
const announcedIds = new Set([...announced.keys()].map(listingIdentity));
|
|
582
|
+
const added = entries.filter((e) => !announcedIds.has(listingIdentity(e.name)));
|
|
583
|
+
const removed = [...announced.keys()].filter((n) => !entries.some((e) => listingIdentity(e.name) === listingIdentity(n)));
|
|
568
584
|
if (added.length === 0 && removed.length === 0)
|
|
569
585
|
return undefined;
|
|
570
586
|
const blocks = [];
|
|
571
587
|
if (added.length > 0)
|
|
572
588
|
blocks.push(`${SKILLS_LISTING_DELTA_HEADER}\n${added.map((e) => skillListingLine(e)).join("\n")}`);
|
|
573
589
|
if (removed.length > 0) {
|
|
574
|
-
blocks.push(`${SKILLS_LISTING_REMOVED_HEADER}\n${removed.map((n) => `- ${n}`).join("\n")}`);
|
|
590
|
+
blocks.push(`${SKILLS_LISTING_REMOVED_HEADER}\n${removed.map((n) => `- ${sanitizeUntrustedText(n, SHELLED_BODY_ENVELOPE_TAGS)}`).join("\n")}`);
|
|
575
591
|
blocks.push(AMBIENT_CONTEXT_NOTE);
|
|
576
592
|
}
|
|
577
593
|
return blocks.join("\n\n");
|
|
@@ -581,12 +597,10 @@ export function commitSkillsListing(state, entries) {
|
|
|
581
597
|
}
|
|
582
598
|
export const MCP_INSTRUCTIONS_MAX_CHARS = 8 * 1024;
|
|
583
599
|
export function fenceMcpServerInstructions(server, text) {
|
|
584
|
-
const
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
? `${fenced}\n(Truncated by the agent runtime: this server's instructions exceeded ${MCP_INSTRUCTIONS_MAX_CHARS} characters.)`
|
|
589
|
-
: fenced;
|
|
600
|
+
const fence = delimitUntrustedWithClip(`MCP server "${server}" instructions`, text, MCP_INSTRUCTIONS_MAX_CHARS);
|
|
601
|
+
return fence.clipped
|
|
602
|
+
? `${fence.text}\n(Truncated by the agent runtime: this server's instructions exceeded ${MCP_INSTRUCTIONS_MAX_CHARS} characters.)`
|
|
603
|
+
: fence.text;
|
|
590
604
|
}
|
|
591
605
|
export function renderMcpInstructionsDelta(added, removed) {
|
|
592
606
|
const blocks = [];
|
|
@@ -383,6 +383,24 @@ export interface ToolPolicyProjection {
|
|
|
383
383
|
*/
|
|
384
384
|
readonly requiresLiveRemainder: boolean;
|
|
385
385
|
}
|
|
386
|
+
/**
|
|
387
|
+
* The entries of a tool-NAME list whose reach is a set of names rather than one name — the MCP
|
|
388
|
+
* covering spellings (`mcp__<server>`, `mcp__<server>__<glob>`) that CC's rule matcher resolves and
|
|
389
|
+
* an exact-membership test silently cannot. Every name-keyed lane in this file consults these
|
|
390
|
+
* ALONGSIDE its exact set: a covering spelling can never equal a minted tool name (`*` is outside the
|
|
391
|
+
* minted charset and a minted name always carries a tool segment), so leaving it in the exact set too
|
|
392
|
+
* costs nothing and keeps the projection/audit faces reporting the operator's own spelling.
|
|
393
|
+
*
|
|
394
|
+
* The list is usually empty, which is why every call site tests it before scanning.
|
|
395
|
+
*
|
|
396
|
+
* Exported for the OTHER name-keyed lane in this engine — the persisted session/ancestor rules in
|
|
397
|
+
* `session-rule-policy.ts`. Not part of the public API (`src/index.ts` re-exports by name and does not
|
|
398
|
+
* list these): all four publishers of {@link ToolPolicyNameSets} must resolve a covering entry the same
|
|
399
|
+
* way, because the prepare-time audit's exemption for these spellings speaks for all of them at once.
|
|
400
|
+
*/
|
|
401
|
+
export declare function mcpCoveringEntries(entries: readonly string[] | undefined): readonly string[];
|
|
402
|
+
/** Does any covering entry reach `toolName`? See {@link mcpCoveringEntries}. */
|
|
403
|
+
export declare function mcpCoveringHit(covering: readonly string[], toolName: string): boolean;
|
|
386
404
|
/**
|
|
387
405
|
* Execute a persisted {@link ToolPolicyProjection} against a call (F-012 L1): returns the first
|
|
388
406
|
* component's deny, or `undefined` when the projection has no opinion (it is deny-only by
|
package/dist/core/tool-policy.js
CHANGED
|
@@ -5,7 +5,7 @@ import { join, normalize as normalizePath, posix as posixPath, sep, win32 as win
|
|
|
5
5
|
import { BASH_READONLY_DEFAULT_ALLOW, parseLeadingCommandName } from "../tools/fs/index.js";
|
|
6
6
|
import { boundInputHashOf } from "./canonical-json.js";
|
|
7
7
|
import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY } from "./untrusted-text.js";
|
|
8
|
-
import { parsePermissionRule } from "./permission-rules.js";
|
|
8
|
+
import { isMcpCoveringRuleName, mcpRuleNameCovers, parsePermissionRule } from "./permission-rules.js";
|
|
9
9
|
import { isAbsolutePathForm, isWinFormPath, writeTargetPath } from "../tools/fs/safety.js";
|
|
10
10
|
const DECISION_REASONS = ["rule", "mode", "hook", "safety", "classifier", "persisted_rule", "sandbox", "org_rule", "org_unavailable"];
|
|
11
11
|
const DECISION_REASON_SET = new Set(DECISION_REASONS);
|
|
@@ -36,16 +36,22 @@ export const ASK_EVIDENCE_ABSENCE_VALUES = ["not_wired", "not_adjudicated", "una
|
|
|
36
36
|
export function decisionText(d) {
|
|
37
37
|
return d.message;
|
|
38
38
|
}
|
|
39
|
+
export function mcpCoveringEntries(entries) {
|
|
40
|
+
return (entries ?? []).filter(isMcpCoveringRuleName);
|
|
41
|
+
}
|
|
42
|
+
export function mcpCoveringHit(covering, toolName) {
|
|
43
|
+
return covering.length > 0 && covering.some((e) => mcpRuleNameCovers(e, toolName));
|
|
44
|
+
}
|
|
39
45
|
export function checkToolPolicyProjection(projection, req) {
|
|
40
46
|
for (const c of projection.components) {
|
|
41
47
|
if (c.kind === "tool_deny") {
|
|
42
|
-
if (c.names.includes(req.toolName)) {
|
|
48
|
+
if (c.names.includes(req.toolName) || mcpCoveringHit(mcpCoveringEntries(c.names), req.toolName)) {
|
|
43
49
|
return { action: "deny", message: `tool "${req.toolName}" is denied by a frozen inherited policy projection` };
|
|
44
50
|
}
|
|
45
51
|
continue;
|
|
46
52
|
}
|
|
47
53
|
if (c.kind === "tool_allowlist") {
|
|
48
|
-
if (!c.names.includes(req.toolName)) {
|
|
54
|
+
if (!c.names.includes(req.toolName) && !mcpCoveringHit(mcpCoveringEntries(c.names), req.toolName)) {
|
|
49
55
|
return { action: "deny", message: `tool "${req.toolName}" is not in a frozen inherited policy projection's allowlist` };
|
|
50
56
|
}
|
|
51
57
|
continue;
|
|
@@ -218,6 +224,8 @@ export function createAllowDenyPolicy(opts) {
|
|
|
218
224
|
opts = { ...opts, ...(screenedAllow ? { allow: screenedAllow } : {}), ...(screenedDeny ? { deny: screenedDeny } : {}) };
|
|
219
225
|
const allow = opts.allow ? new Set(opts.allow) : undefined;
|
|
220
226
|
const deny = new Set(opts.deny ?? []);
|
|
227
|
+
const denyCovering = mcpCoveringEntries(opts.deny);
|
|
228
|
+
const allowCovering = mcpCoveringEntries(opts.allow);
|
|
221
229
|
return {
|
|
222
230
|
projection: {
|
|
223
231
|
components: [
|
|
@@ -229,10 +237,10 @@ export function createAllowDenyPolicy(opts) {
|
|
|
229
237
|
nameSets: [{ ...(opts.allow ? { allow: [...opts.allow] } : {}), ...(opts.deny ? { deny: [...opts.deny] } : {}) }],
|
|
230
238
|
check(req) {
|
|
231
239
|
const toolName = req.toolName;
|
|
232
|
-
if (deny.has(toolName)) {
|
|
240
|
+
if (deny.has(toolName) || mcpCoveringHit(denyCovering, toolName)) {
|
|
233
241
|
return { action: "deny", message: `tool "${req.toolName}" is denied by policy` };
|
|
234
242
|
}
|
|
235
|
-
if (allow && !allow.has(toolName)) {
|
|
243
|
+
if (allow && !allow.has(toolName) && !mcpCoveringHit(allowCovering, toolName)) {
|
|
236
244
|
return { action: "deny", message: `tool "${req.toolName}" is not in the allowlist` };
|
|
237
245
|
}
|
|
238
246
|
return ALLOW;
|
|
@@ -249,6 +257,9 @@ export function createApprovalPolicy(opts) {
|
|
|
249
257
|
const need = new Set(opts.requireApproval);
|
|
250
258
|
const deny = new Set(opts.deny ?? []);
|
|
251
259
|
const auto = new Set(opts.autoAllow ?? []);
|
|
260
|
+
const needCovering = mcpCoveringEntries(opts.requireApproval);
|
|
261
|
+
const denyCovering = mcpCoveringEntries(opts.deny);
|
|
262
|
+
const autoCovering = mcpCoveringEntries(opts.autoAllow);
|
|
252
263
|
return {
|
|
253
264
|
projection: {
|
|
254
265
|
components: [
|
|
@@ -266,10 +277,10 @@ export function createApprovalPolicy(opts) {
|
|
|
266
277
|
],
|
|
267
278
|
async check(req, signal) {
|
|
268
279
|
const toolName = req.toolName;
|
|
269
|
-
if (deny.has(toolName)) {
|
|
280
|
+
if (deny.has(toolName) || mcpCoveringHit(denyCovering, toolName)) {
|
|
270
281
|
return { action: "deny", message: `tool "${req.toolName}" is denied by policy` };
|
|
271
282
|
}
|
|
272
|
-
if (need.has(toolName)) {
|
|
283
|
+
if (need.has(toolName) || mcpCoveringHit(needCovering, toolName)) {
|
|
273
284
|
if (signal?.aborted) {
|
|
274
285
|
return withCoreMintedResolution({ action: "deny", message: `approval aborted for "${req.toolName}" (task ended)`, settledBy: "aborted" }, "task_aborted", req);
|
|
275
286
|
}
|
|
@@ -303,7 +314,7 @@ export function createApprovalPolicy(opts) {
|
|
|
303
314
|
}
|
|
304
315
|
return withCoreMintedResolution({ action: "deny", message: `approval denied for "${req.toolName}"`, settledBy: "human" }, "human_refused", req);
|
|
305
316
|
}
|
|
306
|
-
if (opts.denyByDefault && !auto.has(toolName)) {
|
|
317
|
+
if (opts.denyByDefault && !auto.has(toolName) && !mcpCoveringHit(autoCovering, toolName)) {
|
|
307
318
|
return { action: "deny", message: `tool "${req.toolName}" requires explicit allow` };
|
|
308
319
|
}
|
|
309
320
|
return ALLOW;
|
package/dist/core/types.d.ts
CHANGED
|
@@ -3342,12 +3342,27 @@ export interface TaskResult {
|
|
|
3342
3342
|
}>;
|
|
3343
3343
|
/** design/319 (B ticket, G9② observation seat) — reminder-disclosure trigger counts for the
|
|
3344
3344
|
* leg, keyed `<outlet>.<form>`: outlets `read` / `notebook` / `pdf` / `mcp` / `webFetch` /
|
|
3345
|
-
* `webSearch`; forms
|
|
3346
|
-
*
|
|
3347
|
-
* `
|
|
3348
|
-
*
|
|
3349
|
-
*
|
|
3350
|
-
*
|
|
3345
|
+
* `webSearch`; forms
|
|
3346
|
+
* · `bare` (bare-form trailer appended — reminder-shaped text without this session's mark),
|
|
3347
|
+
* · `marked` (marked-form trailer — never throttled),
|
|
3348
|
+
* · `bare_throttled` (a bare trailer suppressed by the 60s per-key window),
|
|
3349
|
+
* · `defused` (an MCP/web segment's exact-mark bytes were rewritten — the lane's one sanctioned
|
|
3350
|
+
* byte change, always paired with a `marked` disclosure),
|
|
3351
|
+
* · `envelope` (text shaped like one of the engine's OTHER authority envelopes — the DISCLOSED
|
|
3352
|
+
* subset is `task-notification` / `new-diagnostics` / `user_memory` / `skills`; `scope` is
|
|
3353
|
+
* fenced but not disclosed, since `<scope>…</scope>` is also an ordinary build-file element.
|
|
3354
|
+
* That family carries no mark, so its sentence is positional rather than byte-testable. It
|
|
3355
|
+
* rides ON the reminder copy when both families hit, so `envelope` can be bumped alongside
|
|
3356
|
+
* `marked`/`bare` on one result),
|
|
3357
|
+
* · `envelope_throttled` (an envelope sentence suppressed by its OWN 60s window — the two
|
|
3358
|
+
* families throttle independently, so one may emit while the other reports suppression),
|
|
3359
|
+
* · `mark_echo` (observation ONLY, never a disclosure: the session's mark VALUE appeared in
|
|
3360
|
+
* external bytes with no reminder-shaped tag around it, so the tag-grammar detector cannot
|
|
3361
|
+
* reach it. Nothing is rewritten and no trailer is appended; it is an upper bound on model
|
|
3362
|
+
* exposure for the lanes that do not defuse).
|
|
3363
|
+
* Present only when ≥1 key is non-zero. The map is OPEN by type — fold by key rather than
|
|
3364
|
+
* switching on a closed set. This is the D-4/D-6 re-ruling data (defuse/trailer widening to
|
|
3365
|
+
* Read/Bash/Grep): a reading, never a gate. */
|
|
3351
3366
|
reminderDisclosures?: Record<string, number>;
|
|
3352
3367
|
};
|
|
3353
3368
|
/**
|
|
@@ -3667,6 +3682,36 @@ export type TaskEvent = ({
|
|
|
3667
3682
|
* carry no string discriminator under either name.
|
|
3668
3683
|
*/
|
|
3669
3684
|
errorCode?: string;
|
|
3685
|
+
/**
|
|
3686
|
+
* WHICH call the gate was holding, when this frame is a park-contamination frame — the tool call
|
|
3687
|
+
* id of the gated call. Three conditions, ALL required: `errorCode === "gate.parked"`, the run is
|
|
3688
|
+
* tearing down behind a committed park, and that park binds a tool call at all.
|
|
3689
|
+
*
|
|
3690
|
+
* **WHERE IT COMES FROM — never lifted from the result**, and deliberately unlike its neighbour
|
|
3691
|
+
* {@link errorCode}: that one IS the result's own `details.code`, i.e. tool-authored, because a
|
|
3692
|
+
* tool classifying its own failure is bounded self-description. This field is an assertion about a
|
|
3693
|
+
* DIFFERENT call, so a tool able to author it could point an approval UI at a call nobody is
|
|
3694
|
+
* holding. It is therefore engine-minted — read from the run's committed park holder and handed to
|
|
3695
|
+
* the frame projection as a parameter, the same posture (and the same reason) as {@link settledBy}.
|
|
3696
|
+
* `errorCode` only participates in selecting WHETHER the id is placed; it never supplies the
|
|
3697
|
+
* value, and on its own it is not enough — the run must also be aborting behind a real park, which
|
|
3698
|
+
* is why a tool declaring `gate.parked` on its own failure cannot conjure this field.
|
|
3699
|
+
*
|
|
3700
|
+
* Why it is on the frame: a durable gate parks ONE call and the loop then short-circuits every
|
|
3701
|
+
* sibling in the batch with an identical "Operation aborted" body, so `gate.parked` alone says
|
|
3702
|
+
* "something parked this batch" without saying WHAT. A consumer wanting to render "waiting on
|
|
3703
|
+
* <the gated call>" beside the collateral frames otherwise has to re-derive the pair from batch
|
|
3704
|
+
* adjacency and timing — an inference that is wrong exactly when it matters (a batch with more
|
|
3705
|
+
* than one ask candidate, a reordered stream).
|
|
3706
|
+
*
|
|
3707
|
+
* ONLY EVER STRUCTURALLY PROVEN, never guessed: the id is read off the committed checkpoint's
|
|
3708
|
+
* pending tool call, so it is the same value the checkpoint parks on and the same value the
|
|
3709
|
+
* decide/resume lane answers about. It is therefore ABSENT — not approximated — for the park
|
|
3710
|
+
* kinds that hold no call (a resource-limit slice, a plan review), and absent for every abort
|
|
3711
|
+
* that is not a gate park at all (user interrupt, timeout, walltime): those frames are
|
|
3712
|
+
* byte-unchanged. Absence means "core cannot prove a causal call", never "there wasn't one".
|
|
3713
|
+
*/
|
|
3714
|
+
gatedCallId?: string;
|
|
3670
3715
|
/**
|
|
3671
3716
|
* WHAT ENDED THE APPROVAL this call was waiting on, when this frame closes a gated call — `"human"`
|
|
3672
3717
|
* / `"timeout"` / `"aborted"`, the vocabulary of
|
|
@@ -4889,6 +4934,27 @@ export interface EngineNotice {
|
|
|
4889
4934
|
* tier is NOT auto-downgraded (declaration-制 — observation reports, it never re-adjudicates);
|
|
4890
4935
|
* `detail: { handle }`.
|
|
4891
4936
|
*
|
|
4937
|
+
* - `"memory.consolidation_recommended"` (design/339 §2.2/§6.2) — the engine-minted per-scope
|
|
4938
|
+
* session count crossed the consolidation thresholds (time gate open ∧ enough distinct
|
|
4939
|
+
* sessions); minted at most once per crossing (a completed run re-arms the edge), NEVER when
|
|
4940
|
+
* the deployment leaves consolidation off. ADVISORY: the host owns the verbs, nothing runs
|
|
4941
|
+
* automatically; `detail: { scope, sessionsSince, sessionId? }`.
|
|
4942
|
+
* - `"memory.consolidation_committed"` (design/339 §6.2) — a consolidation plan reached
|
|
4943
|
+
* `completed`: products landed, superseded targets left the default read face (retained as
|
|
4944
|
+
* evidence), intents settled; `detail: { planId, scope, products, superseded, intents }` —
|
|
4945
|
+
* counts and engine-minted ids only, zero content.
|
|
4946
|
+
* - `"memory.consolidation_conflict"` (design/339 §6.2) — a plan parked CONFLICT: at least one
|
|
4947
|
+
* target did not reach its planned state (a concurrent write, a patch the store declined, or a
|
|
4948
|
+
* target the re-judgment could no longer act on — the plan's audit rows carry the per-target
|
|
4949
|
+
* reason). The REFUSED targets were not overwritten; patches the plan had already applied
|
|
4950
|
+
* stand (discard rolls nothing back — the receipt's applied-vs-conflict counts say how many).
|
|
4951
|
+
* Affected intents stay pending and wait for the host valve
|
|
4952
|
+
* (`resolveConsolidationPlan` retry/discard); `detail: { planId, scope }`.
|
|
4953
|
+
* - `"memory.consolidation_refused"` (design/339 §6.2) — the notice dialect of a consolidation
|
|
4954
|
+
* verb's coded structured refusal (the verb itself throws
|
|
4955
|
+
* {@link import("../core/memory-engine/consolidation.js").ConsolidationRefusedError});
|
|
4956
|
+
* `detail: { refusalCode, scope? }`.
|
|
4957
|
+
*
|
|
4892
4958
|
* Deliberately NOT a notice family: brain retry/reconnect liveness (a rate limit, a 5xx, a
|
|
4893
4959
|
* transient network failure being retried). Those are per-attempt liveness frames with their own
|
|
4894
4960
|
* frequency semantics and ride the wire `status` channel ({@link BrainStatus}), whose sink the
|
|
@@ -5110,6 +5176,23 @@ export interface RunnerDeps {
|
|
|
5110
5176
|
* never truthiness).
|
|
5111
5177
|
*/
|
|
5112
5178
|
memoryProvenance?: "off" | "carry";
|
|
5179
|
+
/**
|
|
5180
|
+
* design/339 §6.1 — the v3 memory-consolidation switch, threaded verbatim to the engine seat
|
|
5181
|
+
* ({@link import("../core/memory-engine/engine.js").MemoryEngineOptions.consolidation} — same
|
|
5182
|
+
* transport as {@link memoryProvenance}). ABSENT = OFF (the shipped default): no gate state is
|
|
5183
|
+
* ever written, no recommendation is ever minted, and the consolidation verbs on an engine the
|
|
5184
|
+
* host constructs over the same store refuse coded. PRESENT = enabled: the engine counts
|
|
5185
|
+
* distinct terminal-harvest sessions per scope and surfaces the advisory
|
|
5186
|
+
* `memory.consolidation_recommended` notice when the configured thresholds cross — EXECUTION
|
|
5187
|
+
* stays host-owned (the four verbs are engine host API; nothing in a task can trigger a run).
|
|
5188
|
+
* `provenance: "off"` beside this refuses loudly at prepare
|
|
5189
|
+
* (`config.memory_consolidation_provenance_off` — the fold law must be able to mint);
|
|
5190
|
+
* `multiNode: true` without a lease refuses (`config.memory_consolidation_lease_required`);
|
|
5191
|
+
* every other bad value refuses under `config.memory_consolidation` (#123). DEPLOYMENT seat
|
|
5192
|
+
* ONLY (no TaskSpec twin, not in the governed workflow whitelist), same law as
|
|
5193
|
+
* {@link memoryProvenance}.
|
|
5194
|
+
*/
|
|
5195
|
+
memoryConsolidation?: import("../core/memory-engine/consolidation.js").MemoryConsolidationOptions;
|
|
5113
5196
|
/**
|
|
5114
5197
|
* design/199 件A — the DEPLOYMENT's read-face declaration
|
|
5115
5198
|
* ({@link import("../tools/fs/read-face.js").ReadFace}; see {@link TaskSpec.readFace} for the
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { delimitUntrusted } from "./untrusted-text.js";
|
|
1
|
+
import { delimitUntrusted, neutralizeForFence } from "./untrusted-text.js";
|
|
2
2
|
import { scrubSecrets, SECRET_PASSES, runRedactionPasses } from "./arg-summary.js";
|
|
3
3
|
import { sliceHeadSafe } from "./surrogate-safe-slice.js";
|
|
4
4
|
export { summarizeRedactions } from "./arg-summary.js";
|
|
@@ -98,5 +98,15 @@ export function boundedRedactedSummary(value, max) {
|
|
|
98
98
|
return boundedString(value, max, redactSecrets);
|
|
99
99
|
}
|
|
100
100
|
export function untrustedEgressForHuman(value, opts) {
|
|
101
|
-
|
|
101
|
+
const source = boundedString(value, Number.POSITIVE_INFINITY, redactHostLeaks);
|
|
102
|
+
let budget = opts.max;
|
|
103
|
+
let bounded = boundedString(source, budget, (s) => s);
|
|
104
|
+
for (let i = 0; i < 4 && budget > 0; i++) {
|
|
105
|
+
const overflow = [...neutralizeForFence(bounded)].length - opts.max;
|
|
106
|
+
if (overflow <= 0)
|
|
107
|
+
break;
|
|
108
|
+
budget = Math.max(0, budget - overflow);
|
|
109
|
+
bounded = boundedString(source, budget, (s) => s);
|
|
110
|
+
}
|
|
111
|
+
return delimitUntrusted(opts.label, bounded, opts.max);
|
|
102
112
|
}
|
|
@@ -16,6 +16,144 @@
|
|
|
16
16
|
* own (instructions and data share one channel). The real boundary is decorrelation + reading the objective
|
|
17
17
|
* artifact (the diff / working tree) rather than the worker's self-report (design/53 §3, design/54 §3).
|
|
18
18
|
*/
|
|
19
|
+
/**
|
|
20
|
+
* One row of {@link ENGINE_ENVELOPES}.
|
|
21
|
+
*
|
|
22
|
+
* WHY A TABLE EXISTS. design/319 spent its whole threat model on a single tag and three independent
|
|
23
|
+
* texts stated the supporting premise as "this harness mints no other tag" — a MODULE-level
|
|
24
|
+
* enumeration (`turn-attachments` really does mint only one) promoted to a HARNESS-level claim. The
|
|
25
|
+
* engine in fact authors several model-facing envelopes, guarded by two different escape families
|
|
26
|
+
* living in two different modules, and the two envelopes added most recently (`skills`, `scope`)
|
|
27
|
+
* reached the model with NO guard at all — not because anyone argued they were safe, but because
|
|
28
|
+
* nothing in the tree listed the set an author was joining. A guard family without a census is a
|
|
29
|
+
* guard family that silently loses members; this table is that census, and the accompanying
|
|
30
|
+
* literal-tag test keeps it honest (a new closing-tag literal in `src/` fails until it is classified
|
|
31
|
+
* here).
|
|
32
|
+
*
|
|
33
|
+
* The same discipline as the `ToolSpec` completeness table: the registry is the declaration, the
|
|
34
|
+
* test is the gate, and a NEW envelope is a two-line edit rather than an invisible omission.
|
|
35
|
+
*/
|
|
36
|
+
export interface EngineEnvelope {
|
|
37
|
+
/** The tag NAME as the engine spells it, without angle brackets. */
|
|
38
|
+
readonly tag: string;
|
|
39
|
+
/**
|
|
40
|
+
* - `authority` — the engine authors it as model-facing HARNESS speech; seeing the tag is what
|
|
41
|
+
* makes the model read the content as system information, so a forgery is an authority claim.
|
|
42
|
+
* - `framing` — the engine wraps UNTRUSTED content in it as a labeled data frame; a forgery
|
|
43
|
+
* escapes the frame (or closes it early) rather than claiming harness authority.
|
|
44
|
+
* - `not-an-envelope` — a tag-shaped literal in `src/` that is not an engine-minted model-facing
|
|
45
|
+
* envelope at all (a MODEL-output grammar the engine parses, a prompt-assembly placeholder,
|
|
46
|
+
* rendering metadata, or copy that merely TALKS about an envelope). Listed so the census test
|
|
47
|
+
* has a home for every literal in the tree and no reader has to re-derive the classification.
|
|
48
|
+
*/
|
|
49
|
+
readonly kind: "authority" | "framing" | "not-an-envelope";
|
|
50
|
+
/** Where the engine authors (or, for `not-an-envelope`, spells) it. */
|
|
51
|
+
readonly mint: string;
|
|
52
|
+
/** How untrusted bytes reaching this envelope's own body/attributes are defused at that site. */
|
|
53
|
+
readonly guard: string;
|
|
54
|
+
/**
|
|
55
|
+
* True ⇒ neutralized inside every FENCED/inline untrusted projection ({@link delimitUntrusted},
|
|
56
|
+
* {@link inlineUntrusted}). Those lanes already rewrite bytes by contract, so a forged authority
|
|
57
|
+
* envelope arriving through them is defused rather than merely fenced.
|
|
58
|
+
*/
|
|
59
|
+
readonly fenced: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* True ⇒ DISCLOSED (never rewritten) when it appears in VERBATIM external data — the design/319
|
|
62
|
+
* trailer pipeline (reminder-disclosure.ts). Verbatim lanes are byte-frozen by the G3 veto (the
|
|
63
|
+
* Read↔Edit `old_string` quote-back loop), so the only available judgment bit is an appended,
|
|
64
|
+
* engine-minted note. Deliberately NOT set for tags whose spelling is common in ordinary data
|
|
65
|
+
* (`scope` is a Maven POM element; `summary` is HTML) — a trailer that fires on every build file
|
|
66
|
+
* teaches the model to ignore trailers.
|
|
67
|
+
*/
|
|
68
|
+
readonly disclosed: boolean;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The census. Rows are grouped by {@link EngineEnvelope.kind}; within a group, by module.
|
|
72
|
+
*
|
|
73
|
+
* KNOWN CEILING (stated, not hidden): the accompanying test enumerates CLOSING-TAG LITERALS in
|
|
74
|
+
* `src/`, so an envelope whose tag name is INTERPOLATED (`<${tag}>`) is invisible to it. Those are
|
|
75
|
+
* enumerated here by hand — `teammate-message`, the observer digest's four event tags, and the
|
|
76
|
+
* dynamic `<{slug}-activity>` wrapper — and each already carries its own escape at its mint site.
|
|
77
|
+
*/
|
|
78
|
+
export declare const ENGINE_ENVELOPES: readonly EngineEnvelope[];
|
|
79
|
+
/** The registry row for a tag name, or `undefined` when the tag is not in the census. */
|
|
80
|
+
export declare function engineEnvelope(tag: string): EngineEnvelope | undefined;
|
|
81
|
+
/** Registry-derived: the envelopes that read as HARNESS AUTHORITY on sight. */
|
|
82
|
+
export declare const ENGINE_AUTHORITY_ENVELOPE_TAGS: readonly string[];
|
|
83
|
+
/**
|
|
84
|
+
* Registry-derived: the tags {@link delimitUntrusted} / {@link inlineUntrusted} neutralize on top of
|
|
85
|
+
* the always-on reminder tag. These lanes carry EXTERNAL/worker bytes into a labeled data frame and
|
|
86
|
+
* already rewrite them by contract, so an authority envelope forged inside them is defused rather
|
|
87
|
+
* than merely fenced — the "forgery can be neutralized, never trusted" direction.
|
|
88
|
+
*
|
|
89
|
+
* Deliberately NOT applied inside {@link sanitizeUntrustedText} itself: that primitive is also used
|
|
90
|
+
* by mint sites over an ALREADY-ASSEMBLED body (the boundary/first-frame reminder wrap runs it over
|
|
91
|
+
* a body that may legitimately contain the engine's own `<skills>` block), so widening the primitive
|
|
92
|
+
* would have the engine defuse its OWN envelopes. Containment belongs to the fence, not the
|
|
93
|
+
* primitive.
|
|
94
|
+
*/
|
|
95
|
+
export declare const FENCED_LANE_ENVELOPE_TAGS: readonly string[];
|
|
96
|
+
/**
|
|
97
|
+
* The same family as {@link FENCED_LANE_ENVELOPE_TAGS}, as a MUTABLE array for every body that will be
|
|
98
|
+
* SHELLED in a marked `<system-reminder>` — the attachment sink, the listing/diagnostics producers,
|
|
99
|
+
* the durable-resume orphan notice, the post-tool-batch hook relay, the git frame.
|
|
100
|
+
*
|
|
101
|
+
* These are not fences. They render deployment/server/model-supplied strings into a body the run loop
|
|
102
|
+
* then wraps in engine authority, so a forged envelope inside one is laundered by the wrapper. The
|
|
103
|
+
* wrapper itself cannot blanket-neutralize the family — it also shells the one body that legitimately
|
|
104
|
+
* IS an envelope (`buildSkillsBlock`'s `<skills>` fence) — so containment is expressed as OWNERSHIP:
|
|
105
|
+
* this full set for every body that owns nothing, minus its own tag for the one that does (see
|
|
106
|
+
* `attachmentEnvelopeTags` in turn-attachments.ts).
|
|
107
|
+
*
|
|
108
|
+
* Allocated ONCE so the sanitizer's memoized break-out regex is keyed by a stable value.
|
|
109
|
+
*/
|
|
110
|
+
export declare const SHELLED_BODY_ENVELOPE_TAGS: string[];
|
|
111
|
+
/**
|
|
112
|
+
* Registry-derived: the tags the design/319 disclosure pipeline reports when they appear in VERBATIM
|
|
113
|
+
* external data (see {@link scanEnvelopeShaped}). `system-reminder` is absent on purpose — the mark
|
|
114
|
+
* pipeline scans it with a stronger (marked vs bare) verdict of its own.
|
|
115
|
+
*/
|
|
116
|
+
export declare const DISCLOSED_ENVELOPE_TAGS: readonly string[];
|
|
117
|
+
/** The result of one envelope-shaped scan of an external projection. */
|
|
118
|
+
export interface EnvelopeShapedScan {
|
|
119
|
+
/** ≥1 disclosed-family envelope tag (open or close) is present. */
|
|
120
|
+
hit: boolean;
|
|
121
|
+
/** The REGISTRY spellings that matched, de-duplicated, in registry order. Registry-owned strings —
|
|
122
|
+
* never the matched bytes — so a caller may render them into trusted copy safely. */
|
|
123
|
+
tags: string[];
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The envelope-family sibling of {@link scanReminderShaped}: detect text shaped like one of the
|
|
127
|
+
* engine's OTHER authority envelopes inside external data. Read-only — verbatim lanes are byte
|
|
128
|
+
* frozen (design/319 G3 veto: the Read↔Edit `old_string` quote-back loop), so the disclosure
|
|
129
|
+
* pipeline's only move is to append an engine-minted note saying what was seen.
|
|
130
|
+
*
|
|
131
|
+
* Grammar is deliberately the same shape as the reminder scan (attribute / case / tag-internal
|
|
132
|
+
* whitespace tolerant, open OR close tag), and the tag SET comes from the registry rather than a
|
|
133
|
+
* hand-copied list, so adding an envelope to {@link ENGINE_ENVELOPES} with `disclosed: true` is the
|
|
134
|
+
* whole change. Same accepted residual as the reminder scan: a tag the grammar does not recognize
|
|
135
|
+
* (fragmented, Unicode-lookalike) is not a hit — defense in depth, not a guarantee.
|
|
136
|
+
*/
|
|
137
|
+
export declare function scanEnvelopeShaped(text: string): EnvelopeShapedScan;
|
|
138
|
+
/**
|
|
139
|
+
* Defuse a caller-declared occurrence of ONE specific envelope tag (opening AND closing) inside
|
|
140
|
+
* content, CC `zZe` @8339092 verbatim: ``t.replace(new RegExp(`<(?=/?${e}(?:[>\\s/]|$))`, "gi"), "<\\")``.
|
|
141
|
+
*
|
|
142
|
+
* Homed here (beside {@link ENGINE_ENVELOPES}) rather than in the observer module that first needed
|
|
143
|
+
* it: "escape one named envelope tag" is a containment primitive, and the registry's guard column
|
|
144
|
+
* points at it. Re-exported from `agents/observer.ts` for its existing importers.
|
|
145
|
+
*
|
|
146
|
+
* Deviation from CC (hardening): the tag is regex-escaped before entering the RegExp — CC
|
|
147
|
+
* interpolates raw, safe only because its slugs are `[a-zA-Z0-9_-]`; ours are too, but we don't rely
|
|
148
|
+
* on the caller for that invariant.
|
|
149
|
+
*
|
|
150
|
+
* NOTE the two sanctioned escape SHAPES in this tree, and why they are not merged: this one inserts
|
|
151
|
+
* a backslash (`<` → `<\`) because it is a byte-level CC anchor on the observer/teammate lanes;
|
|
152
|
+
* {@link sanitizeUntrustedText} inserts a zero-width space because its lanes are read by humans as
|
|
153
|
+
* well. Both are idempotent and both break the tag; a merge would move bytes on one of the two
|
|
154
|
+
* families for no security gain.
|
|
155
|
+
*/
|
|
156
|
+
export declare function escapeEnvelopeTag(tag: string, text: string): string;
|
|
19
157
|
/** design/319 (B ticket) — what one reminder-shaped scan of an external projection found. */
|
|
20
158
|
export interface ReminderShapedScan {
|
|
21
159
|
/** ≥1 reminder-shaped tag (open or close; attribute/case/whitespace tolerant) is present. */
|
|
@@ -87,6 +225,14 @@ export declare function defuseExactMarkInSegments(segments: readonly string[], m
|
|
|
87
225
|
* `<discussion>`, so an untrusted member could emit `</statement>` to break out (search [46] BUG2);
|
|
88
226
|
* the caller passes those tag names. Tag names must be literal (alphanumeric/hyphen) — they are code-supplied
|
|
89
227
|
* wrapper names, never untrusted input. Idempotent for prompt assembly (a defused tag no longer matches).
|
|
228
|
+
*
|
|
229
|
+
* WHY THE ALWAYS-ON SET IS ONE TAG AND NOT THE WHOLE {@link ENGINE_ENVELOPES} AUTHORITY FAMILY: this is a
|
|
230
|
+
* PRIMITIVE, and mint sites also run it over an ALREADY-ASSEMBLED body — the boundary/first-frame reminder
|
|
231
|
+
* wrap sanitizes a body that may legitimately BE the engine's own `<skills>` block, and the memory write
|
|
232
|
+
* scan diffs against it to decide what markup a note may contain. Widening the primitive would have the
|
|
233
|
+
* engine defuse its own envelopes and would silently move an unrelated store's admission rule. Family-wide
|
|
234
|
+
* containment therefore lives at the FENCE ({@link delimitUntrusted} / {@link inlineUntrusted}, via
|
|
235
|
+
* {@link FENCED_LANE_ENVELOPE_TAGS}) and at each envelope's own mint site (the registry's guard column).
|
|
90
236
|
*/
|
|
91
237
|
export declare function sanitizeUntrustedText(text: string, extraTags?: string[]): string;
|
|
92
238
|
/**
|
|
@@ -237,8 +383,13 @@ export declare function defuseControlChars(text: string): string;
|
|
|
237
383
|
* `\s` does NOT fully match: the C1 half carries the 8-bit CSI/OSC/ST forms (U+009B/U+009D/U+009C) a
|
|
238
384
|
* C1-honoring terminal treats like their ESC-prefixed spellings, so leaving them through would let a
|
|
239
385
|
* 'sanitized' value repaint the trusted line it is interpolated onto (adversarial round finding) — caps the
|
|
240
|
-
* length, then applies the same tag-neutralization
|
|
241
|
-
*
|
|
386
|
+
* length, then applies the same tag-neutralization + fence-sentinel (`<<<`/`>>>`) defusing the fenced
|
|
387
|
+
* body gets.
|
|
388
|
+
*
|
|
389
|
+
* The neutralized tag set is {@link FENCED_LANE_ENVELOPE_TAGS} — the engine's whole AUTHORITY envelope
|
|
390
|
+
* family, not the reminder tag alone. A value interpolated onto a TRUSTED line is the cheapest place to
|
|
391
|
+
* open a forged envelope, and this lane rewrites bytes by contract already, so the whole family is
|
|
392
|
+
* defused here.
|
|
242
393
|
* Defense-in-depth, NOT a guarantee (same posture as the rest of this module).
|
|
243
394
|
*/
|
|
244
395
|
export declare function inlineUntrusted(text: string, maxLen?: number): string;
|
|
@@ -250,8 +401,43 @@ export declare function inlineUntrusted(text: string, maxLen?: number): string;
|
|
|
250
401
|
export declare const REVIEWER_NOTE_MAX_BODY = 2048;
|
|
251
402
|
/**
|
|
252
403
|
* Wrap untrusted text in a clearly labeled opaque fence. The consuming prompt should instruct the model to
|
|
253
|
-
* treat everything inside as untrusted data — never as instructions. Sanitizes internally (
|
|
404
|
+
* treat everything inside as untrusted data — never as instructions. Sanitizes internally (authority-envelope
|
|
254
405
|
* neutralization + fence-sentinel defusing, on the body AND the label), so callers may pass raw
|
|
255
406
|
* worker/external text — and labels derived from external identifiers (hostnames, resource URIs).
|
|
407
|
+
*
|
|
408
|
+
* The neutralized tag set is {@link FENCED_LANE_ENVELOPE_TAGS} — the engine's whole AUTHORITY envelope
|
|
409
|
+
* family (design/319 sibling work): a fence declares its contents to be data, but a forged
|
|
410
|
+
* `<task-notification>` / `<user_memory>` / `<skills>` opening inside it was previously reproduced byte
|
|
411
|
+
* for byte, so a model that skims past the fence line reads harness-shaped text with no judgment bit.
|
|
412
|
+
* This lane rewrites bytes by contract (it is the CONTAINED half; the verbatim quote-back lanes stay
|
|
413
|
+
* frozen and are served by the disclosure trailer instead), so the whole family is defused here.
|
|
414
|
+
*
|
|
415
|
+
* A caller that must DISCLOSE a `maxBody` truncation calls {@link delimitUntrustedWithClip} instead —
|
|
416
|
+
* never a re-derived length test of its own (see that function and {@link neutralizeForFence}).
|
|
256
417
|
*/
|
|
257
418
|
export declare function delimitUntrusted(label: string, text: string, maxBody?: number): string;
|
|
419
|
+
/**
|
|
420
|
+
* The EXACT body neutralization {@link delimitUntrusted} applies, exported so a caller that has to
|
|
421
|
+
* MEASURE the fenced body (its length in code points, whether the cap will bite) measures the string
|
|
422
|
+
* the fence really carries instead of re-deriving the transform from its own copy of the tag list.
|
|
423
|
+
*
|
|
424
|
+
* Two copies of that list is precisely what drifted once the fenced lane widened past the reminder tag:
|
|
425
|
+
* a caller pre-counting with the default (reminder-only) set under-measured the wide-set result by one
|
|
426
|
+
* ZWSP per widened-family tag occurrence, so a body got clipped while the truncation disclosure that
|
|
427
|
+
* decision drove was withheld — an attacker-selectable window just under the cap. Idempotent (both
|
|
428
|
+
* halves are), so a caller may hand the neutralized string straight back to the fence.
|
|
429
|
+
*/
|
|
430
|
+
export declare function neutralizeForFence(text: string): string;
|
|
431
|
+
/**
|
|
432
|
+
* {@link delimitUntrusted} plus the clip verdict, for the callers that must DISCLOSE a truncation on a
|
|
433
|
+
* TRUSTED line outside the fence (§25 honesty: a bare ellipsis reads to the model as the source's own
|
|
434
|
+
* text, so a silent clip is a lie about what the source said).
|
|
435
|
+
*
|
|
436
|
+
* `clipped` is reported by the same measurement that performed the cut. A caller re-deriving "would this
|
|
437
|
+
* clip?" from its own transform + its own tag list is the shape that silently broke when the fence's tag
|
|
438
|
+
* set widened; there is no second condition here to keep in sync.
|
|
439
|
+
*/
|
|
440
|
+
export declare function delimitUntrustedWithClip(label: string, text: string, maxBody?: number): {
|
|
441
|
+
text: string;
|
|
442
|
+
clipped: boolean;
|
|
443
|
+
};
|