@sema-agent/core 7.17.7 → 7.19.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 (201) hide show
  1. package/CHANGELOG.md +233 -13
  2. package/dist/agents/send-message-tool.js +1 -0
  3. package/dist/agents/subagent.d.ts +1 -0
  4. package/dist/agents/subagent.js +5 -0
  5. package/dist/agents/teacher.js +1 -0
  6. package/dist/agents/verify.js +3 -0
  7. package/dist/brain/anthropic.js +6 -3
  8. package/dist/brain/open-responses.js +10 -4
  9. package/dist/brain/openai.js +5 -2
  10. package/dist/brain/reasoning.d.ts +13 -13
  11. package/dist/brain/reasoning.js +2 -2
  12. package/dist/brain/route-adjudicator.d.ts +18 -0
  13. package/dist/brain/route-adjudicator.js +16 -0
  14. package/dist/brain/stream-engine.js +2 -1
  15. package/dist/brain/stream-shared.d.ts +3 -0
  16. package/dist/brain/stream-shared.js +1 -0
  17. package/dist/core/agent-definition.d.ts +10 -0
  18. package/dist/core/artifact-host.d.ts +41 -4
  19. package/dist/core/artifact-host.js +18 -2
  20. package/dist/core/ask-origin.d.ts +16 -0
  21. package/dist/core/ask-origin.js +8 -0
  22. package/dist/core/auto-mode-arming.d.ts +6 -0
  23. package/dist/core/auto-mode-arming.js +5 -5
  24. package/dist/core/auto-mode-defaults.d.ts +43 -15
  25. package/dist/core/auto-mode-defaults.js +4 -1
  26. package/dist/core/auto-mode.d.ts +184 -59
  27. package/dist/core/auto-mode.js +188 -13
  28. package/dist/core/checkpoint-store.d.ts +23 -2
  29. package/dist/core/checkpoint-store.js +2 -2
  30. package/dist/core/engine-notice.d.ts +77 -1
  31. package/dist/core/engine-notice.js +7 -0
  32. package/dist/core/gate-fold.js +5 -1
  33. package/dist/core/gate-lanes.js +9 -8
  34. package/dist/core/gate-outcome.d.ts +42 -2
  35. package/dist/core/gate-outcome.js +23 -0
  36. package/dist/core/governance-codes.d.ts +1 -1
  37. package/dist/core/governance-codes.js +14 -0
  38. package/dist/core/hook-seats.d.ts +66 -0
  39. package/dist/core/hook-seats.js +37 -0
  40. package/dist/core/hooks.d.ts +1 -1
  41. package/dist/core/read-root-candidate.d.ts +12 -0
  42. package/dist/core/read-root-candidate.js +24 -0
  43. package/dist/core/roles.d.ts +9 -1
  44. package/dist/core/roles.js +4 -0
  45. package/dist/core/runner/advertised-writable-dirs.d.ts +8 -3
  46. package/dist/core/runner/assemble-result.d.ts +2 -1
  47. package/dist/core/runner/assemble-result.js +1 -1
  48. package/dist/core/runner/classifier-ladder.d.ts +227 -0
  49. package/dist/core/runner/classifier-ladder.js +214 -0
  50. package/dist/core/runner/contracts.d.ts +71 -13
  51. package/dist/core/runner/decide-continuation.d.ts +1 -3
  52. package/dist/core/runner/decide-continuation.js +1 -7
  53. package/dist/core/runner/deferral-census.d.ts +52 -0
  54. package/dist/core/runner/deferral-census.js +44 -0
  55. package/dist/core/runner/denial-limit-arms.js +2 -2
  56. package/dist/core/runner/derived-route-fallback.d.ts +8 -4
  57. package/dist/core/runner/derived-route-fallback.js +8 -3
  58. package/dist/core/runner/gate-exit.d.ts +12 -0
  59. package/dist/core/runner/gate-exit.js +6 -3
  60. package/dist/core/runner/prepare-artifact.d.ts +10 -2
  61. package/dist/core/runner/prepare-artifact.js +5 -4
  62. package/dist/core/runner/prepare-ask-lane.js +1 -0
  63. package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -2
  64. package/dist/core/runner/prepare-caps-and-workflow.js +43 -55
  65. package/dist/core/runner/prepare-config-doors.d.ts +7 -1
  66. package/dist/core/runner/prepare-config-doors.js +13 -13
  67. package/dist/core/runner/prepare-defer-classify.d.ts +13 -19
  68. package/dist/core/runner/prepare-defer-classify.js +8 -3
  69. package/dist/core/runner/prepare-gate-stations.d.ts +26 -0
  70. package/dist/core/runner/prepare-gate-stations.js +1 -1
  71. package/dist/core/runner/prepare-hands-readface.d.ts +24 -7
  72. package/dist/core/runner/prepare-hands-readface.js +60 -24
  73. package/dist/core/runner/prepare-inherited-gate.d.ts +17 -4
  74. package/dist/core/runner/prepare-inherited-gate.js +10 -2
  75. package/dist/core/runner/prepare-lsp.d.ts +2 -2
  76. package/dist/core/runner/prepare-lsp.js +1 -1
  77. package/dist/core/runner/prepare-policy-chain.js +2 -0
  78. package/dist/core/runner/prepare-project-context.d.ts +5 -1
  79. package/dist/core/runner/prepare-project-context.js +7 -4
  80. package/dist/core/runner/prepare-prompt-inputs.d.ts +4 -2
  81. package/dist/core/runner/prepare-prompt-inputs.js +4 -1
  82. package/dist/core/runner/prepare-run-refs.d.ts +9 -1
  83. package/dist/core/runner/prepare-run-refs.js +3 -2
  84. package/dist/core/runner/prepare-suspend-saga.d.ts +5 -0
  85. package/dist/core/runner/prepare-suspend-saga.js +5 -2
  86. package/dist/core/runner/prepare-task.js +19 -14
  87. package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +5 -0
  88. package/dist/core/runner/prepare-tool-disclosure-mount.js +10 -2
  89. package/dist/core/runner/prepare-turn-wiring.d.ts +8 -4
  90. package/dist/core/runner/prepare-turn-wiring.js +3 -2
  91. package/dist/core/runner/prepare-wiring-manifest.d.ts +5 -5
  92. package/dist/core/runner/prepare-wiring-manifest.js +5 -2
  93. package/dist/core/runner/resume-apply.js +3 -3
  94. package/dist/core/runner/resume-checkpoint-screen.js +7 -0
  95. package/dist/core/runner/run-harness-handlers.js +4 -1
  96. package/dist/core/runner/run-identity-wiring.js +6 -2
  97. package/dist/core/runner/run-leg.d.ts +2 -0
  98. package/dist/core/runner/run-leg.js +45 -45
  99. package/dist/core/runner/run-notification-lane.js +55 -27
  100. package/dist/core/runner/run-settle-and-teardown.js +1 -0
  101. package/dist/core/runner/run-terminal-adoption.js +16 -13
  102. package/dist/core/runner/run-turn-boundary.js +15 -5
  103. package/dist/core/runner/runtask.d.ts +6 -5
  104. package/dist/core/runner/runtask.js +26 -14
  105. package/dist/core/runner/skill-base-dir.d.ts +54 -0
  106. package/dist/core/runner/skill-base-dir.js +42 -0
  107. package/dist/core/runner/synthetic-tools.d.ts +83 -48
  108. package/dist/core/runner/synthetic-tools.js +69 -32
  109. package/dist/core/runner/tool-defer-gate.d.ts +22 -9
  110. package/dist/core/runner/tool-defer-gate.js +15 -5
  111. package/dist/core/runner/tool-disclosure.d.ts +5 -0
  112. package/dist/core/runner/tool-disclosure.js +2 -0
  113. package/dist/core/runner/tool-progress-lane.d.ts +42 -0
  114. package/dist/core/runner/tool-progress-lane.js +61 -0
  115. package/dist/core/runner/turn-attachments.d.ts +43 -12
  116. package/dist/core/runner/turn-attachments.js +58 -14
  117. package/dist/core/runner-deps.d.ts +48 -7
  118. package/dist/core/sensitive-path-policy.d.ts +58 -17
  119. package/dist/core/sensitive-path-policy.js +33 -5
  120. package/dist/core/skill-listing-budget.d.ts +112 -0
  121. package/dist/core/skill-listing-budget.js +58 -0
  122. package/dist/core/swappable-deps.d.ts +9 -7
  123. package/dist/core/swappable-deps.js +7 -3
  124. package/dist/core/task-event.d.ts +96 -29
  125. package/dist/core/task-notification.d.ts +142 -16
  126. package/dist/core/task-notification.js +106 -0
  127. package/dist/core/task-result.d.ts +9 -1
  128. package/dist/core/task-spec.d.ts +46 -3
  129. package/dist/core/tighten-task-spec.js +7 -5
  130. package/dist/core/tool-model-gate.d.ts +1 -1
  131. package/dist/core/tool-policy.d.ts +19 -4
  132. package/dist/core/tool-policy.js +6 -7
  133. package/dist/core/tool-search-setting.d.ts +78 -0
  134. package/dist/core/tool-search-setting.js +90 -0
  135. package/dist/core/tool-spec.d.ts +45 -4
  136. package/dist/core/types.d.ts +2 -2
  137. package/dist/core/types.js +1 -1
  138. package/dist/core/wiring-manifest.d.ts +46 -0
  139. package/dist/core/wiring-manifest.js +9 -1
  140. package/dist/core/write-protect.d.ts +8 -3
  141. package/dist/core/write-protect.js +8 -5
  142. package/dist/engine/execution-env/node-execution-env.js +10 -4
  143. package/dist/engine/harness/agent-harness.d.ts +42 -0
  144. package/dist/engine/harness/agent-harness.js +61 -29
  145. package/dist/engine/loop/agent-loop.d.ts +68 -3
  146. package/dist/engine/loop/agent-loop.js +109 -16
  147. package/dist/index.d.ts +13 -8
  148. package/dist/index.js +11 -7
  149. package/dist/internal/harness.d.ts +1 -1
  150. package/dist/orchestration/run-workflow-tool.d.ts +3 -0
  151. package/dist/orchestration/run-workflow-tool.js +6 -1
  152. package/dist/orchestration/workflow-governance.d.ts +7 -1
  153. package/dist/orchestration/workflow-governance.js +9 -5
  154. package/dist/orchestration/workflow-primitives.d.ts +3 -0
  155. package/dist/orchestration/workflow-primitives.js +6 -1
  156. package/dist/orchestration/workflow-types.d.ts +48 -13
  157. package/dist/orchestration/workflow-types.js +1 -1
  158. package/dist/orchestration/workflow.js +45 -17
  159. package/dist/prompt-assembly/epoch.js +2 -0
  160. package/dist/prompt-assembly/packs/sema-default.js +2 -2
  161. package/dist/prompt-assembly/section-usage.d.ts +26 -0
  162. package/dist/prompt-assembly/section-usage.js +10 -0
  163. package/dist/prompt-assembly/types.d.ts +8 -6
  164. package/dist/prompts/default.d.ts +13 -0
  165. package/dist/prompts/default.js +16 -8
  166. package/dist/tools/artifact/artifact-text.d.ts +10 -1
  167. package/dist/tools/artifact/artifact-text.js +6 -3
  168. package/dist/tools/artifact/artifact-tool.d.ts +12 -2
  169. package/dist/tools/artifact/artifact-tool.js +203 -70
  170. package/dist/tools/artifact/conformance.d.ts +56 -0
  171. package/dist/tools/artifact/conformance.js +349 -0
  172. package/dist/tools/artifact/local-stub.js +1 -1
  173. package/dist/tools/artifact/reject-breaker.d.ts +28 -0
  174. package/dist/tools/artifact/reject-breaker.js +42 -0
  175. package/dist/tools/fs/bash-lexer.d.ts +132 -0
  176. package/dist/tools/fs/bash-lexer.js +563 -0
  177. package/dist/tools/fs/bash-program-position.d.ts +142 -0
  178. package/dist/tools/fs/bash-program-position.js +125 -0
  179. package/dist/tools/fs/bash-readonly-classifier.d.ts +81 -16
  180. package/dist/tools/fs/bash-readonly-classifier.js +180 -558
  181. package/dist/tools/fs/containment-face.d.ts +89 -0
  182. package/dist/tools/fs/containment-face.js +81 -0
  183. package/dist/tools/fs/fs-bash.d.ts +46 -1
  184. package/dist/tools/fs/fs-bash.js +165 -25
  185. package/dist/tools/fs/fs-read.d.ts +1 -1
  186. package/dist/tools/fs/fs-search-tools.d.ts +3 -3
  187. package/dist/tools/fs/fs-write.d.ts +4 -3
  188. package/dist/tools/fs/fs-write.js +9 -9
  189. package/dist/tools/fs/index.d.ts +24 -5
  190. package/dist/tools/fs/index.js +18 -6
  191. package/dist/tools/fs/read-deny.d.ts +4 -3
  192. package/dist/tools/fs/repo-map.d.ts +2 -2
  193. package/dist/tools/fs/safety.d.ts +18 -3
  194. package/dist/tools/fs/safety.js +11 -4
  195. package/package.json +1 -1
  196. package/test/export-surface.snapshot.json +99 -13
  197. package/wire-changes/core@7.17.7.json +49 -0
  198. package/wire-changes/core@7.18.0.json +1273 -0
  199. package/wire-changes/core@7.19.0.json +457 -0
  200. package/dist/tools/fs/read-face.d.ts +0 -68
  201. package/dist/tools/fs/read-face.js +0 -47
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The run's TOOL-PROGRESS lane — the single mint of the `tool_progress` frame: its clock, its cadence,
3
+ * and the heartbeat that keeps a silent long-running tool call visible.
4
+ *
5
+ * TWO PRODUCERS, ONE MINT. A tool that observes its own work ticks fast and carries what it produced: the
6
+ * shell's execute leg watches its command's output and calls `emit` about once a second. Every OTHER call is
7
+ * opaque here — the runner knows only that it started and has not ended — so this lane runs a slow beat for
8
+ * any in-flight call silent for {@link TOOL_HEARTBEAT_MS}.
9
+ *
10
+ * THE LANE OWNS TIME, the producer owns FACTS. A producer reports what it has seen (the tail, the totals),
11
+ * never how long it has run: `elapsedTimeSeconds` is computed HERE from one origin per call, because the two
12
+ * producers do not start their clocks together — the station registers a call when it ENTERS EXECUTION, a
13
+ * shell command's own clock could only start at its spawn. With two origins a 30 s beat could be followed
14
+ * moments later by a shell tick reporting 1 s, breaking the monotonicity the frame promises.
15
+ *
16
+ * THE CADENCE HAS ONE ENFORCEMENT POINT, same reason: a producer's timer is a SAMPLING clock, not the
17
+ * contract. `emit` drops a frame landing inside {@link TOOL_PROGRESS_MIN_INTERVAL_MS} of that call's last.
18
+ *
19
+ * ONE RULE FOR THE BEAT: a call is beaten when nothing has been emitted FOR IT for a whole interval,
20
+ * counting its own start as the first such event — covering both "it only just started" and "the tool
21
+ * already ticks faster than this". The interval is the RUN's, so a first beat lands anywhere in
22
+ * [TOOL_HEARTBEAT_MS, 2 × TOOL_HEARTBEAT_MS): the price of one timer per run, stated rather than hidden.
23
+ *
24
+ * THE CLOCK IS MONOTONIC: every predicate here is a DIFFERENCE of two readings (is this call due a beat, is
25
+ * this offer inside the floor, how long has it run), and off the wall clock a backward step — NTP, a
26
+ * container's clock settling after a resume — makes each difference negative and silences BOTH producers
27
+ * until it catches up: the exact failure this lane exists to prevent.
28
+ *
29
+ * LIVENESS ONLY — see the `tool_progress` member's contract in `task-event.ts`. The timer is `unref`'d and
30
+ * every emission is fault-contained. */
31
+ import type { TaskEvent, TaskEventIdentity } from "../types.js";
32
+ import type { ToolProgressLane } from "./contracts.js";
33
+ /** How long a tool call may stay silent before this lane beats for it, in ms. ONE SOURCE, and CC
34
+ * 2.1.259's own per-tool heartbeat interval (`Cct`) — this engine keeps the cadence and folds the beat
35
+ * into the one progress frame instead of minting a second shape (registered divergence). */
36
+ export declare const TOOL_HEARTBEAT_MS = 30000;
37
+ /** The floor between two frames of ONE call — the `tool_progress.cadence` contract, enforced here and
38
+ * nowhere else so it holds across producers rather than inside each one. */
39
+ export declare const TOOL_PROGRESS_MIN_INTERVAL_MS = 1000;
40
+ /** Build the lane over the run's content pusher (`pushContent` — the ONE seat that puts a frame on this
41
+ * run's queue and, for a sub-agent, forwards it to the parent's sink) and its identity mint. */
42
+ export declare function createToolProgressLane(push: (e: TaskEvent) => void, ident: () => TaskEventIdentity): ToolProgressLane;
@@ -0,0 +1,61 @@
1
+ export const TOOL_HEARTBEAT_MS = 30_000;
2
+ export const TOOL_PROGRESS_MIN_INTERVAL_MS = 1_000;
3
+ const monotonicNow = () => (typeof performance !== "undefined" ? performance.now() : Date.now());
4
+ export function createToolProgressLane(push, ident) {
5
+ const inFlight = new Map();
6
+ let timer;
7
+ const arm = () => {
8
+ if (timer !== undefined)
9
+ return;
10
+ timer = setInterval(beat, TOOL_HEARTBEAT_MS);
11
+ timer.unref?.();
12
+ };
13
+ const emit = (body) => {
14
+ const now = monotonicNow();
15
+ let live = inFlight.get(body.toolCallId);
16
+ if (live === undefined) {
17
+ live = { toolName: body.toolName, startedAt: now, lastAt: now - TOOL_PROGRESS_MIN_INTERVAL_MS };
18
+ inFlight.set(body.toolCallId, live);
19
+ arm();
20
+ }
21
+ if (now - live.lastAt < TOOL_PROGRESS_MIN_INTERVAL_MS)
22
+ return;
23
+ live.lastAt = now;
24
+ try {
25
+ push({ type: "tool_progress", ...body, elapsedTimeSeconds: Math.floor((now - live.startedAt) / 1000), ...ident() });
26
+ }
27
+ catch {
28
+ }
29
+ };
30
+ function beat() {
31
+ const now = monotonicNow();
32
+ for (const [toolCallId, live] of inFlight) {
33
+ if (now - live.lastAt < TOOL_HEARTBEAT_MS)
34
+ continue;
35
+ emit({ toolCallId, toolName: live.toolName });
36
+ }
37
+ }
38
+ const disarm = () => {
39
+ if (timer === undefined)
40
+ return;
41
+ clearInterval(timer);
42
+ timer = undefined;
43
+ };
44
+ return {
45
+ emit,
46
+ noteStart(toolCallId, toolName) {
47
+ const startedAt = monotonicNow();
48
+ inFlight.set(toolCallId, { toolName, startedAt, lastAt: startedAt });
49
+ arm();
50
+ },
51
+ noteEnd(toolCallId) {
52
+ inFlight.delete(toolCallId);
53
+ if (inFlight.size === 0)
54
+ disarm();
55
+ },
56
+ clear() {
57
+ inFlight.clear();
58
+ disarm();
59
+ },
60
+ };
61
+ }
@@ -1,6 +1,7 @@
1
1
  import type { McpDroppedTool } from "../mcp.js";
2
2
  import type { TaskSpec } from "../types.js";
3
- import { type SkillListingEntry } from "./synthetic-tools.js";
3
+ import { type SkillListingEntry, type SkillsBlockRender } from "./synthetic-tools.js";
4
+ import { type ResolvedSkillListingBudget } from "../skill-listing-budget.js";
4
5
  /**
5
6
  * Turn-boundary attachment seam: the PURE producer layer (CC `getContextAttachments`
6
7
  * parity). This module is the single home of the CC-parity cadence judgment + reminder copy (提示词主权批:
@@ -545,6 +546,9 @@ export interface AttachmentInputs {
545
546
  * the delta path serves dynamic-installation deployments and tests, same posture as
546
547
  * `agentListing`). Only passed when `config.skillsListing` is on. */
547
548
  skillsListing?: ReadonlyArray<SkillListingEntry>;
549
+ /** The lowered skills-listing budget seats, resolved once at the config door. Absent ⇒ the module
550
+ * defaults, i.e. byte-identical to the pre-seat render. */
551
+ skillsListingBudget?: ResolvedSkillListingBudget;
548
552
  /** G1 续批 mcp_instructions: pending deltas snapshotted from `MaterializedMcp.instructionsDelta`.
549
553
  * CALLER-owned consumption on intact cap survival (same `newTools` discipline). */
550
554
  mcpInstructions?: {
@@ -849,21 +853,48 @@ export declare function commitAgentListing(state: AttachmentState, entries: Read
849
853
  export declare const SKILLS_LISTING_DELTA_HEADER = "New skills are now available:";
850
854
  /** [c209-C] skills removal-arm header — mirrors the agent-listing removal arm's phrasing. */
851
855
  export declare const SKILLS_LISTING_REMOVED_HEADER = "The following skills are no longer available:";
856
+ /** What one {@link renderSkillsListingFrame} call produced: the render's own report, plus the
857
+ * denominator it is against. */
858
+ export interface SkillsListingFrameRender extends SkillsBlockRender {
859
+ /** How many skills this frame set out to list — the whole roster on an initial frame, the ADDED set
860
+ * on a drift frame. The disclosure's denominator, read off the frame that was actually delivered
861
+ * rather than re-derived from the roster. */
862
+ readonly listed: number;
863
+ }
852
864
  /**
853
- * [c209-C] skills_listing producer — the skills counterpart of {@link renderAgentListingDelta}.
854
- * First call (no announced map) = the full `<skills>` disclosure block VERBATIM (buildSkillsBlock —
855
- * the exact bytes that used to sit in the stable system prompt now ride the reminder lane; CC 209
856
- * classic ships its skills listing the same way, as a first-user-turn reminder ~5.9KB, report §5.1).
857
- * Later calls announce only drift, name-keyed like the agent arm: adds under
858
- * {@link SKILLS_LISTING_DELTA_HEADER} with the SAME `- name: description` line shape the block uses,
859
- * removals under {@link SKILLS_LISTING_REMOVED_HEADER} + `- <name>` lines, followed by the
860
- * CC-verbatim {@link AMBIENT_CONTEXT_NOTE} (agent-removal-arm parity). A description change on an
861
- * existing name is not re-announced (name-keyed, agent-arm parity).
865
+ * [c209-C] skills_listing producer — the skills counterpart of {@link renderAgentListingDelta}, with
866
+ * the degradation report the disclosure lane announces. First call (no announced map) = the full
867
+ * `<skills>` disclosure block VERBATIM (the exact bytes that used to sit in the stable system prompt
868
+ * now ride the reminder lane; CC 209 classic ships its skills listing the same way, as a
869
+ * first-user-turn reminder ~5.9KB, report §5.1). Later calls announce only drift, name-keyed like the
870
+ * agent arm: adds under {@link SKILLS_LISTING_DELTA_HEADER} with the SAME `- name: description` line
871
+ * shape the block uses, removals under {@link SKILLS_LISTING_REMOVED_HEADER} + `- <name>` lines,
872
+ * followed by the CC-verbatim {@link AMBIENT_CONTEXT_NOTE} (agent-removal-arm parity). A description
873
+ * change on an existing name is not re-announced (name-keyed, agent-arm parity).
874
+ *
875
+ * ONE RULER FOR EVERY FRAME: the drift frame is measured against the SAME resolved budget as the
876
+ * initial block, arms and joiners included, and its ADD arm degrades through the same three tiers
877
+ * ({@link renderSkillLinesWithin}). An unmeasured drift frame is the block's own failure mode one lane
878
+ * over: a roster's worth of new skills renders past `ATTACHMENT_BYTE_CAP`, `capAttachments` clips the
879
+ * body, a clipped body never equals the intact-survival re-render, and the frame is re-rendered and
880
+ * re-clipped at every boundary for the life of the task. The split: the ambient tail and the removal
881
+ * arm's one-line floor are reserved first (both constants), the ADD arm degrades into what is left,
882
+ * and the removal arm spends whatever the add arm did not — so the frame's size is the resolved budget
883
+ * or, when even the two floors do not fit it, a small constant, never a function of the roster.
884
+ *
885
+ * The report covers the ADD arm only: `nameOnly` / `dropped` name skills this frame listed without a
886
+ * description or could not list at all. A departed name that did not fit is not a listed skill.
862
887
  *
863
888
  * MED-3① discipline: PURE — the announced-set advance is CALLER-owned via {@link commitSkillsListing},
864
- * committed only on first-frame delivery or intact cap survival at a boundary.
889
+ * committed only on first-frame delivery or intact cap survival at a boundary. The commit records
890
+ * EVERY entry, including one this frame dropped: the alternative is a frame re-rendering the same
891
+ * un-listable tail at every boundary, which is the replay this budget exists to end. The `Skill` tool
892
+ * serves a dropped skill, and `skills.listing_truncated` tells the deployment it happened.
865
893
  */
866
- export declare function renderSkillsListingDelta(state: AttachmentState, entries: ReadonlyArray<SkillListingEntry>): string | undefined;
894
+ export declare function renderSkillsListingFrame(state: AttachmentState, entries: ReadonlyArray<SkillListingEntry>, budget?: ResolvedSkillListingBudget): SkillsListingFrameRender | undefined;
895
+ /** The text-only face of {@link renderSkillsListingFrame} — what the delivery lanes render, and what
896
+ * the intact-survival predicate re-renders to compare. */
897
+ export declare function renderSkillsListingDelta(state: AttachmentState, entries: ReadonlyArray<SkillListingEntry>, budget?: ResolvedSkillListingBudget): string | undefined;
867
898
  /** [c209-C] commit half of {@link renderSkillsListingDelta} — same MED-3① contract as
868
899
  * {@link commitAgentListing}: called by the run loop only on first-frame delivery or when the
869
900
  * `skills_listing` block survived the capped bundle INTACT; a clipped/dropped block leaves the
@@ -1,7 +1,8 @@
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_DESC_MAX_CHARS_DEFAULT, SKILLS_BLOCK_MAX_BYTES } from "../skill-listing-budget.js";
5
6
  import { TOOL_SEARCH_NAME as TOOL_SEARCH_TOOL_NAME } from "./tool-disclosure.js";
6
7
  export const TODO_REMINDER_CONFIG = {
7
8
  TURNS_SINCE_WRITE: 10,
@@ -209,7 +210,9 @@ export function collectDueAttachments(state, inp) {
209
210
  const agentListingBody = inp.config.agentListing && inp.agentListing !== undefined
210
211
  ? renderAgentListingDelta(state, inp.agentListing, inp.agentToolName ?? "Agent", inp.agentModels)
211
212
  : undefined;
212
- const skillsListingBody = inp.config.skillsListing && inp.skillsListing !== undefined ? renderSkillsListingDelta(state, inp.skillsListing) : undefined;
213
+ const skillsListingBody = inp.config.skillsListing && inp.skillsListing !== undefined
214
+ ? renderSkillsListingDelta(state, inp.skillsListing, inp.skillsListingBudget)
215
+ : undefined;
213
216
  const listingPair = [];
214
217
  if (agentListingBody !== undefined)
215
218
  listingPair.push({ source: "agent_listing", body: agentListingBody });
@@ -545,10 +548,9 @@ export function renderAgentListingDelta(state, entries, toolName, models) {
545
548
  return blocks.join("\n\n");
546
549
  }
547
550
  const announcedIds = new Set([...announced.keys()].map(listingIdentity));
551
+ const entryIds = new Set(entries.map((e) => listingIdentity(e.name)));
548
552
  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();
553
+ const removed = [...announced.keys()].filter((n) => !entryIds.has(listingIdentity(n))).sort();
552
554
  const modelsDrifted = state.announcedModels !== undefined &&
553
555
  models !== undefined &&
554
556
  (state.announcedModels.length !== models.length ||
@@ -594,26 +596,68 @@ export function commitAgentListing(state, entries, models) {
594
596
  }
595
597
  export const SKILLS_LISTING_DELTA_HEADER = "New skills are now available:";
596
598
  export const SKILLS_LISTING_REMOVED_HEADER = "The following skills are no longer available:";
597
- export function renderSkillsListingDelta(state, entries) {
599
+ const SKILLS_DELTA_BLOCK_JOINER = "\n\n";
600
+ function skillsRemovalArmFloor(count) {
601
+ return [SKILLS_LISTING_REMOVED_HEADER, `(… +${count} more are no longer available)`].join("\n");
602
+ }
603
+ function renderSkillsRemovalArm(removed, maxBytes) {
604
+ const bytes = (s) => Buffer.byteLength(s, "utf8");
605
+ const lines = removed.map((n) => `- ${sanitizeUntrustedText(n, SHELLED_BODY_ENVELOPE_TAGS)}`);
606
+ const head = bytes(SKILLS_LISTING_REMOVED_HEADER);
607
+ const lineCost = (l) => bytes(l) + 1;
608
+ const prefix = new Array(lines.length + 1);
609
+ prefix[0] = 0;
610
+ for (let i = 0; i < lines.length; i++)
611
+ prefix[i + 1] = prefix[i] + lineCost(lines[i]);
612
+ if (head + prefix[lines.length] <= maxBytes)
613
+ return [SKILLS_LISTING_REMOVED_HEADER, ...lines].join("\n");
614
+ for (let m = lines.length - 1; m >= 0; m--) {
615
+ const tail = `(… +${lines.length - m} more are no longer available)`;
616
+ if (head + prefix[m] + lineCost(tail) <= maxBytes)
617
+ return [SKILLS_LISTING_REMOVED_HEADER, ...lines.slice(0, m), tail].join("\n");
618
+ }
619
+ return skillsRemovalArmFloor(lines.length);
620
+ }
621
+ export function renderSkillsListingFrame(state, entries, budget) {
598
622
  const announced = state.announcedSkills;
599
623
  if (announced === undefined) {
600
624
  if (entries.length === 0)
601
625
  return undefined;
602
- return buildSkillsBlock(entries);
626
+ const block = renderSkillsBlock(entries, budget);
627
+ return block === undefined ? undefined : { ...block, listed: entries.length };
603
628
  }
604
629
  const announcedIds = new Set([...announced.keys()].map(listingIdentity));
630
+ const entryIds = new Set(entries.map((e) => listingIdentity(e.name)));
605
631
  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)));
632
+ const removed = [...announced.keys()].filter((n) => !entryIds.has(listingIdentity(n)));
607
633
  if (added.length === 0 && removed.length === 0)
608
634
  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")}`);
635
+ const bytes = (s) => Buffer.byteLength(s, "utf8");
636
+ const joiner = bytes(SKILLS_DELTA_BLOCK_JOINER);
637
+ const frameMaxBytes = Math.min(budget?.blockMaxBytes ?? SKILLS_BLOCK_MAX_BYTES, SKILLS_BLOCK_MAX_BYTES);
638
+ const ambientCost = removed.length > 0 ? joiner + bytes(AMBIENT_CONTEXT_NOTE) : 0;
639
+ const removalReserve = removed.length > 0 ? joiner + bytes(skillsRemovalArmFloor(removed.length)) : 0;
640
+ const addArm = added.length > 0
641
+ ? renderSkillLinesWithin(added, {
642
+ blockMaxBytes: frameMaxBytes - ambientCost - removalReserve,
643
+ descMaxChars: budget?.descMaxChars ?? SKILL_LISTING_DESC_MAX_CHARS_DEFAULT,
644
+ frame: (lines) => [SKILLS_LISTING_DELTA_HEADER, ...lines].join("\n"),
645
+ })
646
+ : undefined;
647
+ const addCost = addArm === undefined ? 0 : bytes(addArm.text) + (removed.length > 0 ? joiner : 0);
648
+ const blocks = addArm === undefined ? [] : [addArm.text];
612
649
  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);
650
+ blocks.push(renderSkillsRemovalArm(removed, frameMaxBytes - ambientCost - addCost), AMBIENT_CONTEXT_NOTE);
615
651
  }
616
- return blocks.join("\n\n");
652
+ return {
653
+ text: blocks.join(SKILLS_DELTA_BLOCK_JOINER),
654
+ nameOnly: addArm?.nameOnly ?? [],
655
+ dropped: addArm?.dropped ?? [],
656
+ listed: added.length,
657
+ };
658
+ }
659
+ export function renderSkillsListingDelta(state, entries, budget) {
660
+ return renderSkillsListingFrame(state, entries, budget)?.text;
617
661
  }
618
662
  export function commitSkillsListing(state, entries) {
619
663
  state.announcedSkills = new Map(entries.map((e) => [e.name, e.description]));
@@ -393,7 +393,7 @@ export interface RunnerDeps {
393
393
  };
394
394
  /**
395
395
  * design/199 件A — the DEPLOYMENT's read-face declaration
396
- * ({@link import("../tools/fs/read-face.js").ReadFace}; see {@link TaskSpec.readFace} for the
396
+ * ({@link import("../tools/fs/containment-face.js").ContainmentFace}; see {@link TaskSpec.readFace} for the
397
397
  * task layer and the resolution order). An interactive single-user product declares
398
398
  * `readFace: "open"` here in one line; the engine default stays "roots" (D-1b). Under org
399
399
  * governance this seat may still open (it is the deployment's own declaration); the task layer
@@ -406,7 +406,19 @@ export interface RunnerDeps {
406
406
  * later prepare reads the new declaration; announced as `config.read_face_swapped`). A host whose
407
407
  * declaration arrives late (a governance source read after boot) swaps it in rather than restarting.
408
408
  */
409
- readFace?: import("../tools/fs/read-face.js").ReadFace;
409
+ readFace?: import("../tools/fs/containment-face.js").ContainmentFace;
410
+ /**
411
+ * The DEPLOYMENT's WRITE-face declaration
412
+ * ({@link import("../tools/fs/containment-face.js").ContainmentFace}; see {@link TaskSpec.writeFace}
413
+ * for the task layer and the resolution order — the SAME order the read seat runs, one body). The
414
+ * engine default stays "roots". Under org governance this seat may still open (it is the deployment's
415
+ * own declaration); the task layer may not. Beside a read-only (verifier) mount this seat's "open"
416
+ * silently CLAMPS to roots and is ANNOUNCED (`config.write_face_deployment_clamped`); TaskSpec.writeFace's
417
+ * own "open" there refuses loudly. INDEPENDENT of {@link readFace}: neither seat reads the other. A LIVE
418
+ * seat — `Runner.swapDeps({ writeFace })` replaces it without a restart (announced as
419
+ * `config.write_face_swapped`).
420
+ */
421
+ writeFace?: import("../tools/fs/containment-face.js").ContainmentFace;
410
422
  /**
411
423
  * design/96 §20 (P1a) — DEPLOYMENT-DECLARED training knowledge cutoff per model id (e.g.
412
424
  * `{ "qwen-3.5-35b": "2025-01", "deepseek-pro": "2024-07" }`). When the resolved model id is present, the
@@ -663,10 +675,28 @@ export interface RunnerDeps {
663
675
  sessionContext?: readonly string[];
664
676
  /** Transcript-window bounds for the classify prompt (defaults: 40 entries / 2000 chars each). */
665
677
  window?: import("./auto-mode-prompt.js").AutoModeWindowOptions;
666
- /** Classify round-trip cap, ms (default 15_000). A round past the cap is `unavailable(timeout)`: the call is
667
- * DENIED with a sentence that says so (CC 2.1.250 form), and the next call is classified afresh — there is no
668
- * failure count and no session latch. */
678
+ /** Classify round-trip cap, ms (default 15_000) — ONE ladder rung (see `classifierFallback`), not the
679
+ * whole classification. A rung past the cap did not rule: the walk moves to the next candidate, and with
680
+ * none left the call is DENIED with a sentence that says so. The next call is classified afresh. */
669
681
  timeoutMs?: number;
682
+ /**
683
+ * What a classification does when the classifier SEAT does not rule (the leg threw / rejected, the cap
684
+ * elapsed, or the reply carried no verdict):
685
+ * - `"main"` (DEFAULT) — re-ask the SAME prompt on this run's own main model, once, and act on what it
686
+ * says; only if that rung does not rule either does the call reach the fail-closed deny. The record
687
+ * says which model answered (`GateDisposition.classifier`) and the first move per (rung, cause)
688
+ * announces `classifier.fallback` to the operator.
689
+ * - `"none"` — one rung: a classifier that does not rule denies the call exactly as it did before this
690
+ * knob existed, at exactly one model call.
691
+ *
692
+ * COST of the default, stated: while the seat is broken every gated call pays a second round-trip
693
+ * (worst case two `timeoutMs` caps); `"none"` buys that back for the deny.
694
+ *
695
+ * A value outside the closed set ({@link import("./auto-mode.js").AUTO_MODE_CLASSIFIER_FALLBACKS}) is
696
+ * REFUSED loudly at prepare (`config.auto_mode_classifier_fallback_invalid`) — never clamped, never read
697
+ * as the default.
698
+ */
699
+ classifierFallback?: import("./auto-mode.js").AutoModeClassifierFallback;
670
700
  /**
671
701
  * The classifier DENIAL LIMIT (CC 2.1.250 `FO`/`AKe`): a run whose classifier keeps blocking falls
672
702
  * back to a PERSON instead of being denied without end. Per run: a `block` first increments the
@@ -1357,8 +1387,9 @@ export interface RunnerDeps {
1357
1387
  * knob. ② The engine's own candidates — `ToolSpec.defer === true`, every REMOTE-PROTOCOL tool name
1358
1388
  * (MCP **and** A2A peer tools), and built-in tools that declare their own `defer` — sit behind ONE
1359
1389
  * threshold gate, all or nothing: deferred iff their inlined size (name + description + schema; the
1360
- * candidate set only — the caller's list, pins and non-candidates are not measured) reaches 10% of the
1361
- * model's context window; below that they ship inline in full and no ToolSearch is mounted.
1390
+ * candidate set only — the caller's list, pins and non-candidates are not measured) reaches the
1391
+ * {@link toolSearch} threshold (10% of the model's context window when that seat is absent); below that
1392
+ * they ship inline in full and no ToolSearch is mounted.
1362
1393
  *
1363
1394
  * `"auto"` is a CANDIDATE-WIDENING knob, not a second gate: it adds every caller spec — undeclared
1364
1395
  * caller tools included — to the candidate set, measured and decided by the same threshold. Engine
@@ -1369,6 +1400,16 @@ export interface RunnerDeps {
1369
1400
  * if this text drifts.
1370
1401
  */
1371
1402
  deferMode?: "auto";
1403
+ /**
1404
+ * Rule ②'s GATE POLICY — when the engine's candidate set ships as placeholders — in three states:
1405
+ * `true` (always), `false` (never; with no caller list no ToolSearch mounts at all), `{mode:"auto",
1406
+ * thresholdPercent}` (the threshold gate at that percent of the model's context window). Absent ⇒
1407
+ * `{mode:"auto", thresholdPercent:10}`. ORTHOGONAL to {@link deferMode}, which decides WHICH tools are
1408
+ * candidates; neither touches rule ① ({@link TaskSpec.deferTools} still defers,
1409
+ * {@link TaskSpec.alwaysLoadTools} still pins first). A value outside the domain refuses at prepare
1410
+ * (`config.tool_search_invalid`), a `thresholdPercent` outside a finite `[0, 100]` included.
1411
+ */
1412
+ toolSearch?: import("./tool-search-setting.js").ToolSearchSetting;
1372
1413
  /**
1373
1414
  * design/84 Seam C — compact-boundary COST optimization (data-gated, default OFF). A FUNCTION seam (a
1374
1415
  * trusted control-plane injection), so it lives on `RunnerDeps` — NOT `TaskSpec`: `TaskSpec` is the
@@ -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,8 +52,22 @@ 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 {@link RECOMMENDED_SENSITIVE_ASK_PATTERNS}
58
+ * (same judge, so the severity drops and the reach does not); a directory also holds the person's own
59
+ * material, and the directory-wide entry turned an explicit instruction to edit `.claude/projects/…`
60
+ * into a refusal nobody could lift (a deny outranks an ask under the fold).
37
61
  */
38
62
  export declare const RECOMMENDED_SENSITIVE_PATTERNS: readonly string[];
63
+ /**
64
+ * The ASK-severity half of the same guard: the agent-configuration DIRECTORIES, on the SAME judge
65
+ * ({@link createSensitivePathPolicy}'s `askPatterns` seat, whose ABSENCE reads as this list) — only the
66
+ * directories, since the configuration FILES inside them are deny material above. It lives HERE and not
67
+ * only on `WRITE_PROTECTED_DEFAULT_TABLE`, which names the same segments but judges LEXICALLY: a
68
+ * symlink with an innocent name walks past that one, so demoting to it alone trades severity for REACH.
69
+ */
70
+ export declare const RECOMMENDED_SENSITIVE_ASK_PATTERNS: readonly string[];
39
71
  /**
40
72
  * HRD-PRM-10 — the engine data root, read from the same knob its two consumers read
41
73
  * (`$AGENT_DATA_DIR ?? ~/.ai-agent`, off the HOST at wiring time, NFC-normalized) and returned in
@@ -53,27 +85,36 @@ export declare const RECOMMENDED_SENSITIVE_PATTERNS: readonly string[];
53
85
  */
54
86
  export declare function dataRootSpellings(explicit: string | undefined): string[];
55
87
  /**
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.
88
+ * Build a composable policy that judges writes (via the guarded fs tools) by their CANONICAL target,
89
+ * at TWO severities from ONE judge. `patterns` is the DENY set — deployment-supplied, no default list
90
+ * is shipped; `askPatterns` is the ASK set, judged the same way and consulted only where the deny set
91
+ * misses, so a path on both denies. Both empty = a no-op allow.
92
+ *
93
+ * @contract sensitive_paths.two_severities_one_judge — ONE canonicalizing judge answers both
94
+ * severities: a deny is a refusal no approver and no persisted allow rule can lift, an ask is one
95
+ * explicit look that either of them may clear. Lowering a path's severity here never lowers its REACH
96
+ * — both tiers see the real target (symlinks resolved, relative spellings resolved against the call's
97
+ * live cwd), which the synchronous write-protection TABLE deliberately does not.
59
98
  *
60
99
  * `env` is the same ExecutionEnv the hands run against (inject the one you pass to the Runner), so
61
100
  * 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.
101
+ * `rootPath` plays the same fallback-base role it plays on the sibling write gate (see
102
+ * {@link import("./fs-write-gate-policy.js").FsWriteGatePolicyOptions.rootPath}); the call's live
103
+ * `ToolCallRequest.cwd` outranks it for the target.
67
104
  *
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".
105
+ * Fail-open by design: a tool with no string `path` returns `allow` here — this guard adds friction on
106
+ * top of the other gates, it is not the only line of defense, and it must not turn an unrelated tool
107
+ * call into a spurious verdict. The verdicts it owns are narrow and certain: "the real target IS a
108
+ * guarded path → deny / ask". (A target it could not RESOLVE is the one fail-CLOSED arm, below.)
72
109
  */
73
110
  export declare function createSensitivePathPolicy(opts: {
74
111
  env: ExecutionEnv;
75
112
  /** `readonly` accepted so {@link RECOMMENDED_SENSITIVE_PATTERNS} can be passed as-is (widening, non-breaking). */
76
113
  patterns: readonly string[];
114
+ /** The ASK-severity set, same syntax and same judge as `patterns`, consulted only where the deny set
115
+ * misses. ABSENT ⇒ {@link RECOMMENDED_SENSITIVE_ASK_PATTERNS}, so a deployment already wiring this
116
+ * guard keeps its reach over those directories without a code change; `[]` = no ask tier. */
117
+ askPatterns?: readonly string[];
77
118
  rootPath?: string;
78
119
  /**
79
120
  * HRD-PRM-10 — the engine data root this deployment runs on. Default
@@ -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",
@@ -32,6 +50,7 @@ export const RECOMMENDED_SENSITIVE_PATTERNS = [
32
50
  ".bash_history",
33
51
  ".zsh_history",
34
52
  ];
53
+ export const RECOMMENDED_SENSITIVE_ASK_PATTERNS = [".claude", ".sema"];
35
54
  const CASE_INSENSITIVE_FS = process.platform === "darwin" || process.platform === "win32";
36
55
  export function dataRootSpellings(explicit) {
37
56
  const raw = explicit ?? process.env.AGENT_DATA_DIR ?? join(homedir(), ".ai-agent");
@@ -77,6 +96,7 @@ function matchSensitive(canonicalKey, compiled, aliasResolved) {
77
96
  }
78
97
  export function createSensitivePathPolicy(opts) {
79
98
  const compiled = compilePatterns(opts.patterns);
99
+ const compiledAsk = compilePatterns(opts.askPatterns ?? RECOMMENDED_SENSITIVE_ASK_PATTERNS);
80
100
  const dataRootPaths = dataRootSpellings(opts.dataRoot);
81
101
  const dataRootKeys = async (signal) => {
82
102
  const keys = [];
@@ -94,7 +114,7 @@ export function createSensitivePathPolicy(opts) {
94
114
  const guardedByName = opts.tools !== undefined ? new Set(opts.tools) : undefined;
95
115
  return {
96
116
  async check(req, signal) {
97
- if (compiled.length === 0)
117
+ if (compiled.length === 0 && compiledAsk.length === 0)
98
118
  return { action: "allow" };
99
119
  if (guardedByName !== undefined ? !guardedByName.has(req.toolName) : !isProtectedWrite(req))
100
120
  return { action: "allow" };
@@ -129,6 +149,14 @@ export function createSensitivePathPolicy(opts) {
129
149
  decisionReason: "safety",
130
150
  };
131
151
  }
152
+ const askHit = matchSensitive(judged, compiledAsk, canon.aliasResolved === true);
153
+ if (askHit) {
154
+ return {
155
+ action: "ask",
156
+ message: `this write's real target resolves inside the guarded directory "${askHit}" — explicit approval required`,
157
+ decisionReason: "safety",
158
+ };
159
+ }
132
160
  return { action: "allow" };
133
161
  },
134
162
  };