@sema-agent/core 7.18.0 → 7.20.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.
Files changed (137) hide show
  1. package/CHANGELOG.md +276 -2
  2. package/dist/agents/subagent.js +12 -20
  3. package/dist/agents/teacher.js +2 -5
  4. package/dist/agents/verify.js +21 -10
  5. package/dist/brain/anthropic.js +8 -3
  6. package/dist/brain/open-responses.js +12 -4
  7. package/dist/brain/openai.js +7 -2
  8. package/dist/brain/reasoning.d.ts +13 -13
  9. package/dist/brain/reasoning.js +2 -2
  10. package/dist/brain/stream-engine.d.ts +4 -0
  11. package/dist/brain/stream-engine.js +6 -1
  12. package/dist/brain/stream-shared.d.ts +3 -0
  13. package/dist/brain/stream-shared.js +1 -0
  14. package/dist/core/ask-origin.d.ts +16 -0
  15. package/dist/core/ask-origin.js +8 -0
  16. package/dist/core/auto-mode-arming.d.ts +6 -0
  17. package/dist/core/auto-mode-arming.js +5 -5
  18. package/dist/core/auto-mode-defaults.d.ts +43 -15
  19. package/dist/core/auto-mode-defaults.js +4 -1
  20. package/dist/core/auto-mode.d.ts +74 -26
  21. package/dist/core/auto-mode.js +80 -11
  22. package/dist/core/checkpoint-store.d.ts +7 -0
  23. package/dist/core/checkpoint-store.js +2 -2
  24. package/dist/core/child-clamp-seats.d.ts +79 -0
  25. package/dist/core/child-clamp-seats.js +29 -0
  26. package/dist/core/engine-notice.d.ts +89 -41
  27. package/dist/core/fs-write-gate-policy.d.ts +17 -9
  28. package/dist/core/fs-write-gate-policy.js +51 -4
  29. package/dist/core/gate-bounded-await.d.ts +38 -0
  30. package/dist/core/gate-bounded-await.js +53 -0
  31. package/dist/core/gate-fold.js +20 -48
  32. package/dist/core/gate-lanes.js +17 -9
  33. package/dist/core/gate-outcome.d.ts +34 -6
  34. package/dist/core/gate-outcome.js +7 -0
  35. package/dist/core/governance-codes.d.ts +16 -1
  36. package/dist/core/governance-codes.js +18 -0
  37. package/dist/core/hooks.d.ts +8 -8
  38. package/dist/core/memory-engine/engine.d.ts +14 -9
  39. package/dist/core/memory-engine/engine.js +21 -8
  40. package/dist/core/memory-engine/layout.d.ts +31 -0
  41. package/dist/core/memory-engine/layout.js +68 -3
  42. package/dist/core/memory-engine/types.d.ts +24 -0
  43. package/dist/core/memory.d.ts +19 -0
  44. package/dist/core/memory.js +12 -0
  45. package/dist/core/read-root-candidate.d.ts +12 -0
  46. package/dist/core/read-root-candidate.js +24 -0
  47. package/dist/core/remote-env.d.ts +32 -0
  48. package/dist/core/remote-env.js +4 -1
  49. package/dist/core/roles.d.ts +9 -1
  50. package/dist/core/roles.js +4 -0
  51. package/dist/core/runner/advertised-writable-dirs.d.ts +59 -1
  52. package/dist/core/runner/advertised-writable-dirs.js +51 -3
  53. package/dist/core/runner/announce-once-ledger.d.ts +15 -0
  54. package/dist/core/runner/announce-once-ledger.js +8 -0
  55. package/dist/core/runner/classifier-ladder.d.ts +154 -11
  56. package/dist/core/runner/classifier-ladder.js +170 -10
  57. package/dist/core/runner/contracts.d.ts +46 -1
  58. package/dist/core/runner/denial-limit-arms.d.ts +14 -1
  59. package/dist/core/runner/denial-limit-arms.js +7 -6
  60. package/dist/core/runner/env-capability-census.d.ts +30 -0
  61. package/dist/core/runner/env-capability-census.js +38 -0
  62. package/dist/core/runner/forward-sink.d.ts +32 -0
  63. package/dist/core/runner/forward-sink.js +41 -0
  64. package/dist/core/runner/gate-exit.d.ts +4 -0
  65. package/dist/core/runner/gate-exit.js +4 -1
  66. package/dist/core/runner/git-status-frame.d.ts +0 -9
  67. package/dist/core/runner/listing-fold.d.ts +57 -0
  68. package/dist/core/runner/listing-fold.js +75 -0
  69. package/dist/core/runner/prepare-ask-lane.js +1 -0
  70. package/dist/core/runner/prepare-caps-and-workflow.js +20 -31
  71. package/dist/core/runner/prepare-config-doors.d.ts +6 -0
  72. package/dist/core/runner/prepare-config-doors.js +8 -10
  73. package/dist/core/runner/prepare-execution-env.d.ts +1 -1
  74. package/dist/core/runner/prepare-execution-env.js +2 -0
  75. package/dist/core/runner/prepare-gate-stations.d.ts +8 -0
  76. package/dist/core/runner/prepare-gate-stations.js +6 -2
  77. package/dist/core/runner/prepare-hands-readface.d.ts +4 -0
  78. package/dist/core/runner/prepare-hands-readface.js +3 -5
  79. package/dist/core/runner/prepare-listings.d.ts +3 -0
  80. package/dist/core/runner/prepare-listings.js +4 -2
  81. package/dist/core/runner/prepare-memory.js +7 -1
  82. package/dist/core/runner/prepare-policy-chain.js +5 -5
  83. package/dist/core/runner/prepare-project-context.d.ts +5 -1
  84. package/dist/core/runner/prepare-project-context.js +3 -2
  85. package/dist/core/runner/prepare-prompt-inputs.d.ts +2 -2
  86. package/dist/core/runner/prepare-run-refs.d.ts +4 -2
  87. package/dist/core/runner/prepare-run-refs.js +5 -4
  88. package/dist/core/runner/prepare-task.js +8 -6
  89. package/dist/core/runner/resume-apply.js +3 -3
  90. package/dist/core/runner/run-clock-and-content.js +3 -7
  91. package/dist/core/runner/run-harness-handlers.js +10 -10
  92. package/dist/core/runner/run-leg.js +31 -16
  93. package/dist/core/runner/run-settle-and-teardown.js +2 -5
  94. package/dist/core/runner/run-turn-boundary.js +44 -25
  95. package/dist/core/runner/runtask.js +2 -1
  96. package/dist/core/runner/synthetic-tools.d.ts +41 -29
  97. package/dist/core/runner/synthetic-tools.js +25 -40
  98. package/dist/core/runner/task-root-canonical.d.ts +45 -0
  99. package/dist/core/runner/task-root-canonical.js +18 -0
  100. package/dist/core/runner/turn-attachments.d.ts +160 -53
  101. package/dist/core/runner/turn-attachments.js +153 -51
  102. package/dist/core/safe-notify.d.ts +5 -0
  103. package/dist/core/safe-notify.js +24 -2
  104. package/dist/core/sensitive-path-policy.d.ts +84 -17
  105. package/dist/core/sensitive-path-policy.js +37 -19
  106. package/dist/core/skill-listing-budget.d.ts +106 -0
  107. package/dist/core/skill-listing-budget.js +57 -0
  108. package/dist/core/skills-directory.js +17 -10
  109. package/dist/core/task-registry.js +2 -1
  110. package/dist/core/task-spec.d.ts +29 -2
  111. package/dist/core/tool-policy.d.ts +31 -5
  112. package/dist/core/tool-policy.js +45 -32
  113. package/dist/core/tool-spec.d.ts +37 -4
  114. package/dist/core/types.d.ts +1 -1
  115. package/dist/core/write-protect.d.ts +105 -20
  116. package/dist/core/write-protect.js +89 -32
  117. package/dist/index.d.ts +8 -7
  118. package/dist/index.js +6 -5
  119. package/dist/orchestration/workflow-primitives.js +10 -20
  120. package/dist/stores/file/memory-store.d.ts +5 -2
  121. package/dist/stores/file/memory-store.js +8 -4
  122. package/dist/tools/fs/bash-readonly-classifier.d.ts +40 -2
  123. package/dist/tools/fs/bash-readonly-classifier.js +376 -41
  124. package/dist/tools/fs/fs-bash.d.ts +1 -1
  125. package/dist/tools/fs/fs-bash.js +19 -28
  126. package/dist/tools/fs/fs-shared.d.ts +22 -0
  127. package/dist/tools/fs/fs-shared.js +30 -0
  128. package/dist/tools/fs/index.d.ts +1 -1
  129. package/dist/tools/fs/index.js +1 -1
  130. package/dist/tools/fs/read-deny.d.ts +4 -3
  131. package/dist/tools/fs/safety.d.ts +15 -0
  132. package/dist/tools/fs/safety.js +7 -0
  133. package/package.json +2 -1
  134. package/test/export-surface.snapshot.json +31 -5
  135. package/wire-changes/core@7.18.0.json +1273 -0
  136. package/wire-changes/core@7.19.0.json +457 -0
  137. package/wire-changes/core@7.20.0.json +568 -0
@@ -1,7 +1,9 @@
1
1
  import { PRESENT_PLAN_TOOL_NAME } from "../present-plan-tool.js";
2
2
  import { delimitUntrustedWithClip, sanitizeUntrustedText, SHELLED_BODY_ENVELOPE_TAGS } from "../untrusted-text.js";
3
3
  import { protocolOf } from "../protocol-table.js";
4
- import { buildSkillsBlock, skillListingLine } from "./synthetic-tools.js";
4
+ import { renderSkillLinesWithin, renderSkillsBlock } from "./synthetic-tools.js";
5
+ import { SKILL_LISTING_DEFAULT_BUDGET } from "../skill-listing-budget.js";
6
+ import { foldListingWithin, foldRemovalArmWithin, removalArmFloor } from "./listing-fold.js";
5
7
  import { TOOL_SEARCH_NAME as TOOL_SEARCH_TOOL_NAME } from "./tool-disclosure.js";
6
8
  export const TODO_REMINDER_CONFIG = {
7
9
  TURNS_SINCE_WRITE: 10,
@@ -23,6 +25,33 @@ export const CHANGED_FILES_MTIME_EPS_MS = 2000;
23
25
  export const ATTACHMENT_BYTE_CAP = 8 * 1024;
24
26
  const PROJECTION_ITEMS_MAX = 50;
25
27
  const PROJECTION_CONTENT_MAX = 80;
28
+ const LISTING_FRAME_FLOOR_BYTES = 768;
29
+ const DATE_CHANGE_MAX_BYTES = 160;
30
+ const WORKFLOW_SIZE_GUIDELINE_CHANGE_MAX_BYTES = 256;
31
+ export const ATTACHMENT_DELIVERY_CLASS = {
32
+ todo_reminder: { class: "clippable" },
33
+ task_reminder: { class: "clippable" },
34
+ tool_search_usage_reminder: { class: "clippable" },
35
+ changed_files: { class: "clippable" },
36
+ plan_mode: { class: "clippable" },
37
+ budget_usd: { class: "clippable" },
38
+ total_tokens_reminder: { class: "clippable" },
39
+ background_tasks: { class: "clippable" },
40
+ tools_delta: { class: "intact", bound: (inp) => toolsDeltaBound(inp) },
41
+ agent_listing: { class: "intact", bound: (inp) => listingFrameBound(inp.listingBudget) + (inp.agentModels !== undefined ? Buffer.byteLength(modelsAvailableLine(inp.agentModels), "utf8") : 0) },
42
+ skills_listing: { class: "intact", bound: (inp) => listingFrameBound(inp.listingBudget) },
43
+ mcp_instructions: { class: "intact", bound: (inp) => mcpInstructionsBound(inp) },
44
+ mcp_dropped_tools: { class: "intact", bound: (inp) => mcpDroppedToolsBound(inp) },
45
+ date_change: { class: "intact", bound: () => DATE_CHANGE_MAX_BYTES },
46
+ instructions_change: { class: "intact", bound: () => INSTRUCTIONS_CHANGE_BYTE_CAP },
47
+ workflow_size_guideline_change: { class: "intact", bound: () => WORKFLOW_SIZE_GUIDELINE_CHANGE_MAX_BYTES },
48
+ };
49
+ export function isIntactSource(source) {
50
+ return ATTACHMENT_DELIVERY_CLASS[source].class === "intact";
51
+ }
52
+ function listingFrameBound(budget) {
53
+ return Math.max((budget ?? SKILL_LISTING_DEFAULT_BUDGET).blockMaxBytes, LISTING_FRAME_FLOOR_BYTES);
54
+ }
26
55
  const ATTACHMENT_TAGS_DEFAULT = [...SHELLED_BODY_ENVELOPE_TAGS];
27
56
  const ATTACHMENT_TAGS_OWNED = {
28
57
  skills_listing: SHELLED_BODY_ENVELOPE_TAGS.filter((t) => t !== "skills"),
@@ -207,21 +236,15 @@ export function collectDueAttachments(state, inp) {
207
236
  (out ??= []).push({ source: "tools_delta", body });
208
237
  }
209
238
  const agentListingBody = inp.config.agentListing && inp.agentListing !== undefined
210
- ? renderAgentListingDelta(state, inp.agentListing, inp.agentToolName ?? "Agent", inp.agentModels)
239
+ ? renderAgentListingDelta(state, inp.agentListing, inp.agentToolName ?? "Agent", inp.agentModels, inp.listingBudget)
211
240
  : undefined;
212
- const skillsListingBody = inp.config.skillsListing && inp.skillsListing !== undefined ? renderSkillsListingDelta(state, inp.skillsListing) : undefined;
213
- const listingPair = [];
214
241
  if (agentListingBody !== undefined)
215
- listingPair.push({ source: "agent_listing", body: agentListingBody });
216
- if (skillsListingBody !== undefined) {
217
- const att = { source: "skills_listing", body: skillsListingBody };
218
- if (state.skillsListingStarved === true)
219
- listingPair.unshift(att);
220
- else
221
- listingPair.push(att);
222
- }
223
- for (const att of listingPair)
224
- (out ??= []).push(att);
242
+ (out ??= []).push({ source: "agent_listing", body: agentListingBody });
243
+ const skillsListingBody = inp.config.skillsListing && inp.skillsListing !== undefined
244
+ ? renderSkillsListingDelta(state, inp.skillsListing, inp.listingBudget)
245
+ : undefined;
246
+ if (skillsListingBody !== undefined)
247
+ (out ??= []).push({ source: "skills_listing", body: skillsListingBody });
225
248
  if (inp.config.mcpInstructions && inp.mcpInstructions !== undefined) {
226
249
  const body = renderMcpInstructionsDelta(inp.mcpInstructions.added, inp.mcpInstructions.removed);
227
250
  if (body !== undefined)
@@ -241,9 +264,6 @@ export function collectDueAttachments(state, inp) {
241
264
  if (out === undefined)
242
265
  return EMPTY;
243
266
  const capped = capAttachments(out);
244
- if (skillsListingBody !== undefined) {
245
- state.skillsListingStarved = !capped.some((a) => a.source === "skills_listing" && a.body === skillsListingBody);
246
- }
247
267
  if (pendingSurfaced !== undefined && capped.some((a) => a.source === "changed_files")) {
248
268
  for (const c of pendingSurfaced)
249
269
  state.surfacedMtime.set(c.path, c.mtimeMs);
@@ -431,15 +451,19 @@ export function renderToolsDelta(input) {
431
451
  const added = input.added ?? [];
432
452
  if (added.length > 0) {
433
453
  const swapped = input.staticFace === true ? (input.swappedUnderStatic ?? []).filter((n) => added.includes(n)) : [];
434
- blocks.push((input.staticFace === true
435
- ? "The following deferred tools are now active — call them directly. The tools list keeps compact " +
436
- 'placeholder entries, so re-run ToolSearch ("select:<name>") if you need a parameter schema:\n'
437
- : "The following deferred tools are now available. Their full schemas are loaded — call them " +
438
- "directly like any other tool:\n") +
439
- added.map((n) => `- ${n}`).join("\n") +
440
- (swapped.length > 0
441
- ? `\nException: ${swapped.join(", ")} — too large to inline, so the full declaration is in the tools list instead.`
442
- : ""));
454
+ const grouped = added.length > TOOLS_DELTA_LIST_MAX;
455
+ const exception = swapped.length > 0
456
+ ? grouped
457
+ ? `\nException: ${swapped.length} of them were too large to inline, so their full declarations are in the tools list instead.`
458
+ : `\nException: ${swapped.join(", ")} — too large to inline, so the full declaration is in the tools list instead.`
459
+ : "";
460
+ blocks.push((grouped ? `${added.length} deferred tools are now ${input.staticFace === true ? "active" : "available"}: ${groupByMcpServer(added)}. ` : "") +
461
+ (input.staticFace === true
462
+ ? (grouped ? "Call them directly. " : "The following deferred tools are now active — call them directly. ") +
463
+ `The tools list keeps compact placeholder entries, so re-run ToolSearch ("select:<name>") if you need a parameter schema${grouped ? "." : ":\n"}`
464
+ : (grouped ? "" : "The following deferred tools are now available. ") + `Their full schemas are loaded — call them directly like any other tool${grouped ? "." : ":\n"}`) +
465
+ (grouped ? "" : added.map((n) => `- ${n}`).join("\n")) +
466
+ exception);
443
467
  }
444
468
  const readded = input.readded ?? [];
445
469
  if (readded.length > 0) {
@@ -527,45 +551,66 @@ function listingIdentity(name) {
527
551
  export function agentListingDeltaHeader(toolName) {
528
552
  return `New agent types are now available for the ${sanitizeUntrustedText(toolName, SHELLED_BODY_ENVELOPE_TAGS)} tool:`;
529
553
  }
530
- export function renderAgentListingDelta(state, entries, toolName, models) {
554
+ export function renderAgentListingFrame(state, entries, toolName, models, budget = SKILL_LISTING_DEFAULT_BUDGET) {
531
555
  const safe = (t) => sanitizeUntrustedText(t, SHELLED_BODY_ENVELOPE_TAGS);
532
556
  const line = (e) => {
533
557
  const base = e.description ? `- ${safe(e.name)}: ${safe(e.description)}` : `- ${safe(e.name)}`;
534
558
  return `${base} (Tools: ${safe(e.tools ?? AGENT_TOOLS_NOTE_DEFAULT)})`;
535
559
  };
560
+ const bytes = (s) => Buffer.byteLength(s, "utf8");
561
+ const joiner = bytes(AGENT_DELTA_BLOCK_JOINER);
562
+ const fold = (items, header, maxBytes) => foldListingWithin(items, {
563
+ blockMaxBytes: maxBytes,
564
+ full: line,
565
+ nameOnly: (e) => `- ${safe(e.name)}`,
566
+ nameOf: (e) => e.name,
567
+ capMarker: AGENT_LISTING_CAP_MARKER,
568
+ overflowLine: (n) => `(… +${n} more agent types not listed)`,
569
+ frame: (lines) => [header, ...lines].join("\n"),
570
+ });
536
571
  const announced = state.announcedAgentTypes;
537
572
  if (announced === undefined) {
538
573
  if (entries.length === 0)
539
574
  return undefined;
540
- const blocks = [`${agentListingInitialHeader(toolName)}\n${entries.map(line).join("\n")}`];
575
+ const tail = [];
541
576
  if (entries.length > 1)
542
- blocks.push(AGENT_CONCURRENCY_NOTE);
577
+ tail.push(AGENT_CONCURRENCY_NOTE);
543
578
  if (models !== undefined && models.length > 0)
544
- blocks.push(modelsAvailableLine(models));
545
- return blocks.join("\n\n");
579
+ tail.push(modelsAvailableLine(models));
580
+ const tailCost = tail.reduce((acc, t) => acc + joiner + bytes(t), 0);
581
+ const roster = fold(entries, agentListingInitialHeader(toolName), budget.blockMaxBytes - tailCost);
582
+ return { text: [roster.text, ...tail].join(AGENT_DELTA_BLOCK_JOINER), nameOnly: roster.nameOnly, dropped: roster.dropped, listed: entries.length };
546
583
  }
547
584
  const announcedIds = new Set([...announced.keys()].map(listingIdentity));
585
+ const entryIds = new Set(entries.map((e) => listingIdentity(e.name)));
548
586
  const added = entries.filter((e) => !announcedIds.has(listingIdentity(e.name))).toSorted((a, b) => a.name.localeCompare(b.name));
549
- const removed = [...announced.keys()]
550
- .filter((n) => !entries.some((e) => listingIdentity(e.name) === listingIdentity(n)))
551
- .sort();
587
+ const removed = [...announced.keys()].filter((n) => !entryIds.has(listingIdentity(n))).sort();
552
588
  const modelsDrifted = state.announcedModels !== undefined &&
553
589
  models !== undefined &&
554
590
  (state.announcedModels.length !== models.length ||
555
591
  state.announcedModels.some((m, i) => listingIdentity(m) !== listingIdentity(models[i])));
556
592
  if (added.length === 0 && removed.length === 0 && !modelsDrifted)
557
593
  return undefined;
558
- const blocks = [];
559
- if (added.length > 0)
560
- blocks.push(`${agentListingDeltaHeader(toolName)}\n${added.map(line).join("\n")}`);
594
+ const modelsLine = modelsDrifted ? modelsAvailableLine(models) : undefined;
595
+ const modelsCost = modelsLine !== undefined ? joiner + bytes(modelsLine) : 0;
596
+ const ambientCost = removed.length > 0 ? joiner + bytes(AMBIENT_CONTEXT_NOTE) : 0;
597
+ const removalReserve = removed.length > 0 ? joiner + bytes(removalArmFloor(AGENT_LISTING_REMOVED_HEADER, removed.length)) : 0;
598
+ const addArm = added.length > 0 ? fold(added, agentListingDeltaHeader(toolName), budget.blockMaxBytes - modelsCost - ambientCost - removalReserve) : undefined;
599
+ const addCost = addArm === undefined ? 0 : bytes(addArm.text) + joiner;
600
+ const blocks = addArm === undefined ? [] : [addArm.text];
561
601
  if (removed.length > 0) {
562
- blocks.push(`${AGENT_LISTING_REMOVED_HEADER}\n${removed.map((n) => `- ${safe(n)}`).join("\n")}`);
602
+ blocks.push(foldRemovalArmWithin(removed, AGENT_LISTING_REMOVED_HEADER, budget.blockMaxBytes - modelsCost - ambientCost - addCost));
563
603
  blocks.push(AMBIENT_CONTEXT_NOTE);
564
604
  }
565
- if (modelsDrifted)
566
- blocks.push(modelsAvailableLine(models));
567
- return blocks.join("\n\n");
605
+ if (modelsLine !== undefined)
606
+ blocks.push(modelsLine);
607
+ return { text: blocks.join(AGENT_DELTA_BLOCK_JOINER), nameOnly: addArm?.nameOnly ?? [], dropped: addArm?.dropped ?? [], listed: added.length };
608
+ }
609
+ export function renderAgentListingDelta(state, entries, toolName, models, budget) {
610
+ return renderAgentListingFrame(state, entries, toolName, models, budget)?.text;
568
611
  }
612
+ const AGENT_LISTING_CAP_MARKER = "(listing budget reached — lower-priority agent types are listed name-only)";
613
+ const AGENT_DELTA_BLOCK_JOINER = "\n\n";
569
614
  export const MODELS_AVAILABLE_PREFIX = "Models available for the 'model' parameter: ";
570
615
  function modelsAvailableLine(models) {
571
616
  const named = models.map((m) => sanitizeUntrustedText(m, SHELLED_BODY_ENVELOPE_TAGS));
@@ -594,26 +639,47 @@ export function commitAgentListing(state, entries, models) {
594
639
  }
595
640
  export const SKILLS_LISTING_DELTA_HEADER = "New skills are now available:";
596
641
  export const SKILLS_LISTING_REMOVED_HEADER = "The following skills are no longer available:";
597
- export function renderSkillsListingDelta(state, entries) {
642
+ const SKILLS_DELTA_BLOCK_JOINER = "\n\n";
643
+ export function renderSkillsListingFrame(state, entries, budget = SKILL_LISTING_DEFAULT_BUDGET) {
598
644
  const announced = state.announcedSkills;
599
645
  if (announced === undefined) {
600
646
  if (entries.length === 0)
601
647
  return undefined;
602
- return buildSkillsBlock(entries);
648
+ const block = renderSkillsBlock(entries, budget);
649
+ return block === undefined ? undefined : { ...block, listed: entries.length };
603
650
  }
604
651
  const announcedIds = new Set([...announced.keys()].map(listingIdentity));
652
+ const entryIds = new Set(entries.map((e) => listingIdentity(e.name)));
605
653
  const added = entries.filter((e) => !announcedIds.has(listingIdentity(e.name)));
606
- const removed = [...announced.keys()].filter((n) => !entries.some((e) => listingIdentity(e.name) === listingIdentity(n)));
654
+ const removed = [...announced.keys()].filter((n) => !entryIds.has(listingIdentity(n)));
607
655
  if (added.length === 0 && removed.length === 0)
608
656
  return undefined;
609
- const blocks = [];
610
- if (added.length > 0)
611
- blocks.push(`${SKILLS_LISTING_DELTA_HEADER}\n${added.map((e) => skillListingLine(e)).join("\n")}`);
657
+ const bytes = (s) => Buffer.byteLength(s, "utf8");
658
+ const joiner = bytes(SKILLS_DELTA_BLOCK_JOINER);
659
+ const frameMaxBytes = budget.blockMaxBytes;
660
+ const ambientCost = removed.length > 0 ? joiner + bytes(AMBIENT_CONTEXT_NOTE) : 0;
661
+ const removalReserve = removed.length > 0 ? joiner + bytes(removalArmFloor(SKILLS_LISTING_REMOVED_HEADER, removed.length)) : 0;
662
+ const addArm = added.length > 0
663
+ ? renderSkillLinesWithin(added, {
664
+ blockMaxBytes: frameMaxBytes - ambientCost - removalReserve,
665
+ descMaxChars: budget.descMaxChars,
666
+ frame: (lines) => [SKILLS_LISTING_DELTA_HEADER, ...lines].join("\n"),
667
+ })
668
+ : undefined;
669
+ const addCost = addArm === undefined ? 0 : bytes(addArm.text) + (removed.length > 0 ? joiner : 0);
670
+ const blocks = addArm === undefined ? [] : [addArm.text];
612
671
  if (removed.length > 0) {
613
- blocks.push(`${SKILLS_LISTING_REMOVED_HEADER}\n${removed.map((n) => `- ${sanitizeUntrustedText(n, SHELLED_BODY_ENVELOPE_TAGS)}`).join("\n")}`);
614
- blocks.push(AMBIENT_CONTEXT_NOTE);
672
+ blocks.push(foldRemovalArmWithin(removed, SKILLS_LISTING_REMOVED_HEADER, frameMaxBytes - ambientCost - addCost), AMBIENT_CONTEXT_NOTE);
615
673
  }
616
- return blocks.join("\n\n");
674
+ return {
675
+ text: blocks.join(SKILLS_DELTA_BLOCK_JOINER),
676
+ nameOnly: addArm?.nameOnly ?? [],
677
+ dropped: addArm?.dropped ?? [],
678
+ listed: added.length,
679
+ };
680
+ }
681
+ export function renderSkillsListingDelta(state, entries, budget) {
682
+ return renderSkillsListingFrame(state, entries, budget)?.text;
617
683
  }
618
684
  export function commitSkillsListing(state, entries) {
619
685
  state.announcedSkills = new Map(entries.map((e) => [e.name, e.description]));
@@ -659,6 +725,37 @@ export function selectMcpDroppedBatch(entries) {
659
725
  }
660
726
  return out;
661
727
  }
728
+ const TOOLS_DELTA_FIXED_MAX_BYTES = 2048;
729
+ function toolsDeltaBound(inp) {
730
+ const bytes = (s) => Buffer.byteLength(s, "utf8");
731
+ const list = (names) => (names.length > TOOLS_DELTA_LIST_MAX ? bytes(groupByMcpServer(names)) : names.reduce((acc, n) => acc + bytes(n) + 3, 0));
732
+ const swapped = (names) => names.reduce((acc, n) => acc + bytes(n) + 2, 0);
733
+ const mcp = inp.mcpToolsDelta;
734
+ return (TOOLS_DELTA_FIXED_MAX_BYTES +
735
+ list(inp.newTools ?? []) +
736
+ swapped(inp.newToolsSwappedUnderStatic ?? []) +
737
+ bytes(groupByMcpServer(mcp?.readded ?? [])) +
738
+ swapped(inp.readdedToolsSwappedUnderStatic ?? []) +
739
+ list(mcp?.removed ?? []) +
740
+ (mcp?.failedServers ?? []).slice(0, TOOLS_DELTA_LIST_MAX).reduce((acc, f) => acc + bytes(f.name) + bytes(f.error ?? "") + 8, 0));
741
+ }
742
+ const MCP_INSTRUCTIONS_FENCE_MAX_BYTES = 512;
743
+ function mcpInstructionsBound(inp) {
744
+ const bytes = (s) => Buffer.byteLength(s, "utf8");
745
+ const m = inp.mcpInstructions;
746
+ if (m === undefined)
747
+ return 0;
748
+ return (512 +
749
+ m.added.reduce((acc, a) => acc + MCP_INSTRUCTIONS_MAX_CHARS * 4 + bytes(a.server) + MCP_INSTRUCTIONS_FENCE_MAX_BYTES, 0) +
750
+ m.removed.reduce((acc, n) => acc + bytes(n) + 1, 0));
751
+ }
752
+ const MCP_DROPPED_FIXED_MAX_BYTES = 1024;
753
+ function mcpDroppedToolsBound(inp) {
754
+ const bytes = (s) => Buffer.byteLength(s, "utf8");
755
+ const first = inp.mcpDroppedTools?.[0];
756
+ const single = first === undefined ? 0 : MCP_DROPPED_FIXED_MAX_BYTES + bytes(first.tool) + bytes(first.server) + bytes(first.reason) + 32;
757
+ return Math.max(MCP_DROPPED_BATCH_MAX_BYTES, single);
758
+ }
662
759
  const PLAN_MODE_FULL_BODY = `Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits, run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supercedes any other instructions you have received. Instead, you should: research and analyze with read-only tools, and when you have formed a concrete plan, present it with the ${PRESENT_PLAN_TOOL_NAME} tool for approval. That tool inherently requests plan approval — never ask for approval via plain text.`;
663
760
  const PLAN_MODE_SPARSE_BODY = `Plan mode still active (see full instructions earlier in conversation). Read-only: no edits, commits, or config changes. When your plan is ready, present it with the ${PRESENT_PLAN_TOOL_NAME} tool for approval. Never ask about plan approval via plain text.`;
664
761
  const TRUNCATION_MARKER = "\n…[truncated]";
@@ -666,12 +763,17 @@ const MIN_KEEP_BYTES = 160;
666
763
  function capAttachments(atts) {
667
764
  let total = 0;
668
765
  for (const a of atts)
669
- total += Buffer.byteLength(a.body, "utf8");
766
+ if (!isIntactSource(a.source))
767
+ total += Buffer.byteLength(a.body, "utf8");
670
768
  if (total <= ATTACHMENT_BYTE_CAP)
671
769
  return atts;
672
770
  let budget = ATTACHMENT_BYTE_CAP;
673
771
  const out = [];
674
772
  for (const a of atts) {
773
+ if (isIntactSource(a.source)) {
774
+ out.push(a);
775
+ continue;
776
+ }
675
777
  const len = Buffer.byteLength(a.body, "utf8");
676
778
  if (len <= budget) {
677
779
  out.push(a);
@@ -56,6 +56,11 @@ export interface SafeNotifierOptions {
56
56
  */
57
57
  readonly onError?: (failure: SafeNotifyFailure) => void;
58
58
  }
59
+ /** The sentence of a contained failure, for a consumer that puts it in front of a person. Total by the
60
+ * same rule as {@link containedError}: an error object handed in from elsewhere (a `lastErrorAt`
61
+ * reading, a rejection an adapter wrapped) may still carry a hostile `message`, and the disclosure a
62
+ * site composes from it must not be the thing that faults. */
63
+ export declare function safeErrorMessage(error: unknown): string;
59
64
  export interface SafeNotifier {
60
65
  /**
61
66
  * Run one host-callback invocation, containing any throw. Returns `true` when it completed, `false` when it
@@ -1,9 +1,30 @@
1
+ function containedError(thrown) {
2
+ try {
3
+ if (thrown instanceof Error && typeof thrown.message === "string")
4
+ return thrown;
5
+ return new Error(String(thrown));
6
+ }
7
+ catch {
8
+ return new Error(`a thrown value of type ${typeof thrown} that cannot be inspected or converted to text`);
9
+ }
10
+ }
11
+ export function safeErrorMessage(error) {
12
+ try {
13
+ const m = error?.message;
14
+ if (typeof m === "string")
15
+ return m;
16
+ return String(error);
17
+ }
18
+ catch {
19
+ return "a failure whose message could not be read";
20
+ }
21
+ }
1
22
  export function createSafeNotifier(opts) {
2
23
  const onError = opts?.onError;
3
24
  let sites;
4
25
  let total = 0;
5
26
  const record = (site, thrown) => {
6
- const error = thrown instanceof Error ? thrown : new Error(String(thrown));
27
+ const error = containedError(thrown);
7
28
  total += 1;
8
29
  if (sites === undefined)
9
30
  sites = new Map();
@@ -60,8 +81,9 @@ export function createSafeNotifier(opts) {
60
81
  export function observeThenableRejection(r, notifier, site) {
61
82
  if (typeof r?.then === "function") {
62
83
  r.then(undefined, (err) => {
84
+ const error = containedError(err);
63
85
  notifier.notify(() => {
64
- throw err instanceof Error ? err : new Error(String(err));
86
+ throw error;
65
87
  }, site);
66
88
  });
67
89
  }
@@ -3,6 +3,22 @@ import type { ToolPolicy } from "./tool-policy.js";
3
3
  /** fs hand tools that take a `path` and write — the reliably-guardable surface. NotebookEdit joined
4
4
  * in [886]①a (mirror of the fs-write-gate's covered set): its target is `notebook_path`, and a
5
5
  * guard that ignored notebook writes to the same paths by default was the same hole one tool away. */
6
+ /**
7
+ * The agent-configuration block of {@link RECOMMENDED_SENSITIVE_PATTERNS} MIRRORS the read tier (spelled as
8
+ * literals so the closed set stays statically readable; a set-equality pin keeps the mirror exact): the read tier's rows already encode this severity's predicate ("an agent's own config
9
+ * FILE, whose `env`/`mcpServers` block is a credential or a command line the host launches") and argue
10
+ * each one. Shared is the VOCABULARY, not the judgment.
11
+ *
12
+ * @contract sensitive_paths.agent_config_tier_is_shared — the write deny set's agent-configuration
13
+ * block IS the read face's `agent-config` tier, row for row: one table, two judges, and the two faces
14
+ * cannot drift into disagreeing about which files are an agent's own configuration. The CONFIGURATION
15
+ * DIRECTORIES (`.claude`, `.sema`) are on neither face's deny side — their write guard is ASK
16
+ * severity, where an approver or a persisted allow rule can clear it. Their `hooks/` and `agents/`
17
+ * sub-directories are the two exceptions and they are NOT part of this mirror: a hook script is an executable
18
+ * body and an agent definition re-authorizes the agent on load, both guarded on the write face only, by
19
+ * hand-kept rows of the list below.
20
+ */
21
+ export declare function agentConfigDenyPatterns(): readonly string[];
6
22
  /**
7
23
  * RECOMMENDED default pattern set for {@link createSensitivePathPolicy} (additive export).
8
24
  * NO upstream verbatim anchor exists (cc-anchors/2.1.209 carries no sensitive/dangerous file-pattern
@@ -20,13 +36,15 @@ import type { ToolPolicy } from "./tool-policy.js";
20
36
  *
21
37
  * HRD-PRM-10 — sema's own data root (`$AGENT_DATA_DIR ?? ~/.ai-agent`) is excluded BY CONSTRUCTION,
22
38
  * not by the absence of an entry. The exclusion used to be "just don't add a pattern that names it",
23
- * which held only as long as no entry happened to match one of its ANCESTOR segments. That assumption
24
- * died the moment the shell's own config root joined the list: the shell sets
39
+ * which held only as long as no entry happened to match one of its ANCESTOR segments — and that
40
+ * assumption died the first time a directory-wide config-root entry was on this list: the shell sets
25
41
  * `AGENT_DATA_DIR = <configRoot>/engine-data` with a configRoot of `~/.sema`, so every path under the
26
- * data root carries a `.sema` segment, and a pattern guarding the config root swallowed the whole data
27
- * root with it — including the memory library the model is INSTRUCTED to write with the Write tool
42
+ * data root carries a `.sema` segment, and the entry swallowed the whole data root with it —
43
+ * including the memory library the model is INSTRUCTED to write with the Write tool
28
44
  * (`MEMORY_INSTRUCTION_TEMPLATE`), i.e. every memory write refused, on a default server wiring.
29
- * So the exclusion is now mechanical: a target INSIDE the data root is matched on its path RELATIVE to
45
+ * That entry is gone (see the severity layering below), but the exclusion stays mechanical — a
46
+ * deployment may site its data root beneath ANY guarded directory, and both tiers honour it. A
47
+ * target INSIDE the data root is matched on its path RELATIVE to
30
48
  * that root (see {@link createSensitivePathPolicy}'s `dataRoot`), so the root's own ancestry is not the
31
49
  * target's business, while a genuinely sensitive path INSIDE it (`<dataRoot>/x/.ssh/id_rsa`) still
32
50
  * matches. The `sessions/` subtree keeps its own owner: `createTranscriptIntegrityPolicy` is wired
@@ -34,6 +52,13 @@ import type { ToolPolicy } from "./tool-policy.js";
34
52
  * transcripts is routine» and a legitimate operator write must remain clearable by judgment — and a
35
53
  * deny here would outrank that ask under the deny > ask > allow fold and silently convert it into a
36
54
  * hard refusal for every deployment that wires this list.
55
+ *
56
+ * SEVERITY LAYERING for the agent-configuration class: the law is {@link agentConfigDenyPatterns} —
57
+ * configuration FILES here, the DIRECTORY one step down on the always-on write-protection table
58
+ * (`WRITE_PROTECTED_DEFAULT_TABLE`'s `.claude` / `.sema` rows — the same reach, since that judge sees the
59
+ * real target too, {@link import("./write-protect.js").WriteProtectionMatcher.judge}); a directory also
60
+ * holds the person's own material, and a directory-wide DENY entry turned an explicit instruction to edit
61
+ * `.claude/projects/…` into a refusal nobody could lift (a deny outranks an ask under the fold).
37
62
  */
38
63
  export declare const RECOMMENDED_SENSITIVE_PATTERNS: readonly string[];
39
64
  /**
@@ -53,22 +78,64 @@ export declare const RECOMMENDED_SENSITIVE_PATTERNS: readonly string[];
53
78
  */
54
79
  export declare function dataRootSpellings(explicit: string | undefined): string[];
55
80
  /**
56
- * Build a composable policy that DENIES writes (via the guarded fs tools) whose canonical target
57
- * lands on a sensitive path. `patterns` are deployment-supplied (no default list is shipped — the
58
- * set of escalation-vector paths is deployment-specific); an empty list is a no-op allow.
81
+ * The data root's identity in the SAME namespace a judged key lives in: every declared spelling
82
+ * ({@link dataRootSpellings}) run through the SAME `canonicalizeTarget` that mints the target key. Not a
83
+ * second canonicalization — the same one, on the other operand, which is what lets
84
+ * {@link relativeUnderRoot}'s comparison be exact.
85
+ *
86
+ * Resolved PER CALL, deliberately not cached. A cached canonical root is a snapshot of the symlink
87
+ * topology at first use: with a data root reached through a symlink, retargeting it left the policy
88
+ * still exempting the OLD target for the life of the run. The module header already accepts paying
89
+ * `canonicalizeTarget` twice for a guarded write as the price of a decoupled gate; this is the same
90
+ * bargain on the other operand, and a request-scoped cache is the same note's answer if profiling
91
+ * ever asks for one. A spelling that cannot be resolved contributes NO exemption (the guard's side wins).
92
+ *
93
+ * ONE resolver for both write judges (the deny set here, the always-on table's target view) — an
94
+ * engine-internal seam, like the two neighbours above.
95
+ */
96
+ export declare function canonicalDataRootKeys(env: ExecutionEnv, spellings: readonly string[], signal?: AbortSignal): Promise<string[]>;
97
+ /**
98
+ * Is `targetKey` inside the data root `rootKey`, and if so what is its path relative to it? Returns
99
+ * the relative sub-path (`""` when the target IS the root), or `null` for everything else.
100
+ *
101
+ * 🔴 EXACT, and deliberately nothing else. Both operands are minted by the SAME canonicalizer, so
102
+ * every transformation this comparison could apply — a case fold, an NFC pass, splitting a foreign
103
+ * separator family, unioning several readings of the root — can only ADD matches. On the DENY side of
104
+ * this file that is the safe direction and several of those transforms are applied on purpose (a
105
+ * spelling filter is not a path filter). Here the direction inverts: this test grants an EXEMPTION, so
106
+ * an added match is not a wider guard, it is a bypass of one — each of case folding, an NFC pass and a
107
+ * foreign-family read was measured leaking an unrelated sibling into the exemption (their pins name the
108
+ * shapes). The rule: compare what the canonicalizer produced, byte for byte, every uncertainty falling
109
+ * to the deny side.
110
+ *
111
+ * The separator family comes from the ROOT key (a backslash is an ordinary filename character on
112
+ * POSIX, so a root `/x/.sema/data` must not swallow the sibling FILE `/x/.sema/data\evil`), and a root
113
+ * that reduces to nothing exempts nothing rather than exempting the filesystem.
114
+ *
115
+ * Exported as an ENGINE-INTERNAL seam (not re-exported from `src/index.ts`), like {@link dataRootSpellings}:
116
+ * the always-on write-protection judge applies the same exemption on its target view, and a second
117
+ * containment test would be a second place for the two judges to disagree about what "inside" means.
118
+ */
119
+ export declare function relativeUnderRoot(targetKey: string, rootKey: string): string | null;
120
+ /**
121
+ * Build a composable policy that DENIES writes (via the guarded fs tools) whose CANONICAL target lands on
122
+ * a guarded pattern. `patterns` is deployment-supplied — no default list is shipped; empty = a no-op allow.
123
+ * A deny is a refusal no approver and no persisted allow rule can lift. The ASK severity of the same class
124
+ * (the agent-configuration DIRECTORIES) is not a tier here: it is the always-on write-protection table's,
125
+ * which judges the same real target ({@link import("./write-protect.js").WriteProtectionMatcher.judge} —
126
+ * its contract sentence `write_protection.judges_the_real_target` says the two differ in vocabulary and
127
+ * severity, never in reach).
59
128
  *
60
129
  * `env` is the same ExecutionEnv the hands run against (inject the one you pass to the Runner), so
61
130
  * canonicalization sees the task's real filesystem (a remote/E2B task resolves in its container).
62
- * `rootPath` is the FALLBACK base a relative path resolves against (omit = paths resolve as given).
63
- * When the engine stamps a live cwd onto the request (`ToolCallRequest.cwd` — the tracked
64
- * directory the shell's `cd` moves and the fs tools resolve with), THAT wins for the call's target, so
65
- * the guard judges the file the tool will actually write. `rootPath` still serves every deployment that
66
- * calls this policy outside a Runner.
131
+ * `rootPath` plays the same fallback-base role it plays on the sibling write gate (see
132
+ * {@link import("./fs-write-gate-policy.js").FsWriteGatePolicyOptions.rootPath}); the call's live
133
+ * `ToolCallRequest.cwd` outranks it for the target.
67
134
  *
68
- * Fail-open by design: a tool with no string `path`, or a path that cannot be canonicalized, returns
69
- * `allow` here — this guard adds a denial on top of the other gates, it is not the only line of
70
- * defense, and it must not turn an unrelated tool call into a spurious deny. The verdict it owns is
71
- * narrow and certain: "the real target IS a guarded path → deny".
135
+ * Fail-open by design: a tool with no string `path` returns `allow` here — this guard adds friction on
136
+ * top of the other gates, it is not the only line of defense, and it must not turn an unrelated tool
137
+ * call into a spurious verdict. The verdict it owns is narrow and certain: "the real target IS a
138
+ * guarded path → deny". (A target it could not RESOLVE is the one fail-CLOSED arm, below.)
72
139
  */
73
140
  export declare function createSensitivePathPolicy(opts: {
74
141
  env: ExecutionEnv;
@@ -1,9 +1,12 @@
1
1
  import { homedir } from "node:os";
2
2
  import { isAbsolute, join, resolve } from "node:path";
3
3
  import { canonicalizeTarget, expandHomeTilde, isAbsoluteForFamily, isWinFormPath, pathFamilyOf, writeTargetPath } from "../tools/fs/safety.js";
4
- import { compileSegmentPattern, matchSegmentPatterns } from "../tools/fs/read-deny.js";
4
+ import { compileSegmentPattern, matchSegmentPatterns, READ_FACE_BUILTIN_DENY_TABLE } from "../tools/fs/read-deny.js";
5
5
  import { isProtectedWrite, protectivePathTargetOf } from "./tool-registry.js";
6
6
  import { pathTargetBaseOf } from "./effective-path-target.js";
7
+ export function agentConfigDenyPatterns() {
8
+ return READ_FACE_BUILTIN_DENY_TABLE.filter((row) => row.tier === "agent-config").map((row) => row.pattern);
9
+ }
7
10
  export const RECOMMENDED_SENSITIVE_PATTERNS = [
8
11
  ".env",
9
12
  ".env.*",
@@ -18,10 +21,25 @@ export const RECOMMENDED_SENSITIVE_PATTERNS = [
18
21
  ".git/hooks",
19
22
  ".git/config",
20
23
  ".git/modules",
21
- ".mcp.json",
22
- ".claude*",
23
- ".sema",
24
+ ".sema/settings.json",
25
+ ".sema/settings.local.json",
24
26
  ".sema.*",
27
+ ".claude/settings.json",
28
+ ".claude/settings.local.json",
29
+ ".claude.*",
30
+ ".mcp.json",
31
+ ".ai-agent/.env",
32
+ ".sema/engine-data/.env",
33
+ ".codex/config.toml",
34
+ ".cursor/mcp.json",
35
+ ".continue/config.json",
36
+ ".continue/config.yaml",
37
+ ".aider.conf.yml",
38
+ ".gemini/settings.json",
39
+ ".claude/hooks",
40
+ ".sema/hooks",
41
+ ".claude/agents",
42
+ ".sema/agents",
25
43
  ".sema-notifier-build",
26
44
  ".aws",
27
45
  ".config/gcloud",
@@ -46,7 +64,20 @@ export function dataRootSpellings(explicit) {
46
64
  }
47
65
  return out;
48
66
  }
49
- function relativeUnderRoot(targetKey, rootKey) {
67
+ export async function canonicalDataRootKeys(env, spellings, signal) {
68
+ const keys = [];
69
+ for (const spelling of spellings) {
70
+ try {
71
+ const r = await canonicalizeTarget(env, spelling, signal, pathFamilyOf({ root: spelling }));
72
+ if (r.ok && !keys.includes(r.key))
73
+ keys.push(r.key);
74
+ }
75
+ catch {
76
+ }
77
+ }
78
+ return keys;
79
+ }
80
+ export function relativeUnderRoot(targetKey, rootKey) {
50
81
  const seps = isWinFormPath(rootKey) ? ["\\", "/"] : ["/"];
51
82
  let root = rootKey;
52
83
  while (root.length > 0 && seps.includes(root[root.length - 1]))
@@ -78,19 +109,6 @@ function matchSensitive(canonicalKey, compiled, aliasResolved) {
78
109
  export function createSensitivePathPolicy(opts) {
79
110
  const compiled = compilePatterns(opts.patterns);
80
111
  const dataRootPaths = dataRootSpellings(opts.dataRoot);
81
- const dataRootKeys = async (signal) => {
82
- const keys = [];
83
- for (const spelling of dataRootPaths) {
84
- try {
85
- const r = await canonicalizeTarget(opts.env, spelling, signal, pathFamilyOf({ root: spelling }));
86
- if (r.ok && !keys.includes(r.key))
87
- keys.push(r.key);
88
- }
89
- catch {
90
- }
91
- }
92
- return keys;
93
- };
94
112
  const guardedByName = opts.tools !== undefined ? new Set(opts.tools) : undefined;
95
113
  return {
96
114
  async check(req, signal) {
@@ -113,7 +131,7 @@ export function createSensitivePathPolicy(opts) {
113
131
  };
114
132
  }
115
133
  let relative = null;
116
- for (const rootKey of await dataRootKeys(signal)) {
134
+ for (const rootKey of await canonicalDataRootKeys(opts.env, dataRootPaths, signal)) {
117
135
  relative = relativeUnderRoot(canon.key, rootKey);
118
136
  if (relative !== null)
119
137
  break;