@sema-agent/core 7.33.8 → 7.35.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 (228) hide show
  1. package/CHANGELOG.md +462 -3
  2. package/dist/agents/cross-session-judge.d.ts +16 -7
  3. package/dist/agents/cross-session-judge.js +18 -15
  4. package/dist/agents/peer-session-drain.d.ts +4 -0
  5. package/dist/agents/peer-session-drain.js +5 -2
  6. package/dist/agents/repair-loop.d.ts +1 -1
  7. package/dist/agents/subagent.d.ts +3 -0
  8. package/dist/agents/subagent.js +12 -3
  9. package/dist/brain/anthropic.js +1 -0
  10. package/dist/brain/errors.d.ts +23 -1
  11. package/dist/brain/errors.js +19 -2
  12. package/dist/brain/open-responses.js +1 -0
  13. package/dist/brain/openai.js +1 -0
  14. package/dist/brain/status-sink.d.ts +7 -1
  15. package/dist/brain/stream-engine.d.ts +3 -0
  16. package/dist/brain/stream-engine.js +2 -1
  17. package/dist/brain/stream-shared.d.ts +3 -1
  18. package/dist/brain/stream-shared.js +16 -4
  19. package/dist/core/a2a.d.ts +3 -1
  20. package/dist/core/a2a.js +1 -1
  21. package/dist/core/ask-origin.d.ts +379 -154
  22. package/dist/core/ask-origin.js +224 -45
  23. package/dist/core/ask-question.js +2 -1
  24. package/dist/core/ask-unresolvable-notice.d.ts +5 -2
  25. package/dist/core/ask-unresolvable-notice.js +4 -3
  26. package/dist/core/auto-mode-arming.d.ts +17 -4
  27. package/dist/core/auto-mode-arming.js +31 -6
  28. package/dist/core/auto-mode-defaults.d.ts +16 -7
  29. package/dist/core/auto-mode-defaults.js +2 -1
  30. package/dist/core/auto-mode-directive.d.ts +18 -30
  31. package/dist/core/auto-mode-directive.js +9 -18
  32. package/dist/core/auto-mode-prompt.d.ts +62 -1
  33. package/dist/core/auto-mode-prompt.js +42 -4
  34. package/dist/core/auto-mode-rebuild.d.ts +2 -1
  35. package/dist/core/auto-mode-rebuild.js +1 -0
  36. package/dist/core/auto-mode.d.ts +32 -3
  37. package/dist/core/auto-mode.js +80 -41
  38. package/dist/core/checkpoint-store.d.ts +54 -35
  39. package/dist/core/checkpoint-store.js +58 -3
  40. package/dist/core/child-clamp-seats.js +1 -0
  41. package/dist/core/classifier-unavailable-arm.d.ts +57 -0
  42. package/dist/core/classifier-unavailable-arm.js +17 -0
  43. package/dist/core/effective-path-target.d.ts +12 -0
  44. package/dist/core/effective-path-target.js +18 -7
  45. package/dist/core/engine-notice.d.ts +53 -2
  46. package/dist/core/fs-write-gate-policy.d.ts +5 -1
  47. package/dist/core/fs-write-gate-policy.js +4 -1
  48. package/dist/core/gate-fold.js +19 -3
  49. package/dist/core/gate-lanes.js +116 -50
  50. package/dist/core/gate-outcome.d.ts +40 -7
  51. package/dist/core/gate-outcome.js +26 -4
  52. package/dist/core/governance-codes.d.ts +18 -2
  53. package/dist/core/governance-codes.js +23 -1
  54. package/dist/core/hooks.d.ts +53 -29
  55. package/dist/core/hooks.js +1 -3
  56. package/dist/core/mcp.js +1 -1
  57. package/dist/core/message-utils.d.ts +2 -1
  58. package/dist/core/message-utils.js +2 -1
  59. package/dist/core/mode-policy.d.ts +185 -0
  60. package/dist/core/mode-policy.js +184 -0
  61. package/dist/core/oracle-isolation.d.ts +3 -3
  62. package/dist/core/permission-rule-model.d.ts +20 -3
  63. package/dist/core/permission-rule-model.js +40 -3
  64. package/dist/core/permission-rule-provider.d.ts +22 -11
  65. package/dist/core/permission-rule-provider.js +17 -7
  66. package/dist/core/permission-rules.d.ts +23 -1
  67. package/dist/core/permission-rules.js +56 -9
  68. package/dist/core/persisted-rule-arms.d.ts +14 -10
  69. package/dist/core/persisted-rule-arms.js +13 -6
  70. package/dist/core/present-plan-tool.d.ts +1 -1
  71. package/dist/core/present-plan-tool.js +3 -3
  72. package/dist/core/read-only-shell.d.ts +19 -19
  73. package/dist/core/read-only-shell.js +38 -12
  74. package/dist/core/retired-keys.d.ts +9 -3
  75. package/dist/core/retired-keys.js +5 -1
  76. package/dist/core/runner/active-skill-scope.js +5 -4
  77. package/dist/core/runner/assemble-result.js +2 -2
  78. package/dist/core/runner/classifier-ladder.d.ts +43 -1
  79. package/dist/core/runner/classifier-ladder.js +29 -0
  80. package/dist/core/runner/contracts.d.ts +62 -38
  81. package/dist/core/runner/denial-limit-arms.d.ts +35 -7
  82. package/dist/core/runner/denial-limit-arms.js +15 -9
  83. package/dist/core/runner/env-capability-census.js +1 -1
  84. package/dist/core/runner/file-history-request.d.ts +26 -0
  85. package/dist/core/runner/file-history-request.js +90 -0
  86. package/dist/core/runner/gate-exit.d.ts +30 -4
  87. package/dist/core/runner/gate-exit.js +28 -7
  88. package/dist/core/runner/permission-rule-lanes.d.ts +11 -6
  89. package/dist/core/runner/permission-rule-lanes.js +55 -48
  90. package/dist/core/runner/persisted-path-rows.d.ts +64 -0
  91. package/dist/core/runner/persisted-path-rows.js +89 -0
  92. package/dist/core/runner/prepare-artifact.d.ts +5 -1
  93. package/dist/core/runner/prepare-artifact.js +9 -3
  94. package/dist/core/runner/prepare-ask-lane.d.ts +6 -4
  95. package/dist/core/runner/prepare-ask-lane.js +3 -3
  96. package/dist/core/runner/prepare-caps-and-workflow.d.ts +39 -13
  97. package/dist/core/runner/prepare-caps-and-workflow.js +58 -29
  98. package/dist/core/runner/prepare-file-history.d.ts +9 -3
  99. package/dist/core/runner/prepare-file-history.js +4 -90
  100. package/dist/core/runner/prepare-gate-stations.d.ts +37 -9
  101. package/dist/core/runner/prepare-gate-stations.js +46 -29
  102. package/dist/core/runner/prepare-hands-readface.d.ts +40 -17
  103. package/dist/core/runner/prepare-hands-readface.js +64 -53
  104. package/dist/core/runner/prepare-inherited-gate.d.ts +30 -23
  105. package/dist/core/runner/prepare-inherited-gate.js +45 -25
  106. package/dist/core/runner/prepare-park-ask.d.ts +6 -4
  107. package/dist/core/runner/prepare-park-ask.js +7 -5
  108. package/dist/core/runner/prepare-policy-chain.d.ts +25 -4
  109. package/dist/core/runner/prepare-policy-chain.js +203 -180
  110. package/dist/core/runner/prepare-protocol-tools.d.ts +28 -0
  111. package/dist/core/runner/prepare-protocol-tools.js +13 -3
  112. package/dist/core/runner/prepare-run-refs.d.ts +6 -6
  113. package/dist/core/runner/prepare-run-refs.js +9 -4
  114. package/dist/core/runner/prepare-safety-scan.d.ts +5 -0
  115. package/dist/core/runner/prepare-safety-scan.js +19 -3
  116. package/dist/core/runner/prepare-suspend-saga.d.ts +5 -5
  117. package/dist/core/runner/prepare-suspend-saga.js +6 -6
  118. package/dist/core/runner/prepare-task.js +56 -24
  119. package/dist/core/runner/prepare-wiring-manifest.d.ts +10 -6
  120. package/dist/core/runner/prepare-wiring-manifest.js +5 -3
  121. package/dist/core/runner/read-root-grant.d.ts +9 -0
  122. package/dist/core/runner/read-root-grant.js +12 -0
  123. package/dist/core/runner/retired-runner-deps.js +1 -0
  124. package/dist/core/runner/run-brain-sinks.js +3 -1
  125. package/dist/core/runner/run-identity-wiring.js +1 -1
  126. package/dist/core/runner/run-leg.js +29 -18
  127. package/dist/core/runner/run-notification-lane.d.ts +3 -0
  128. package/dist/core/runner/run-notification-lane.js +3 -1
  129. package/dist/core/runner/run-telemetry-and-budget-seats.d.ts +2 -2
  130. package/dist/core/runner/run-telemetry-and-budget-seats.js +1 -1
  131. package/dist/core/runner/run-turn-boundary.js +1 -1
  132. package/dist/core/runner/runtask.d.ts +1 -0
  133. package/dist/core/runner/runtask.js +8 -11
  134. package/dist/core/runner/session-mode-seats.d.ts +48 -0
  135. package/dist/core/runner/session-mode-seats.js +73 -0
  136. package/dist/core/runner/session-rule-policy.js +3 -2
  137. package/dist/core/runner/stream-mode-verb.d.ts +27 -0
  138. package/dist/core/runner/stream-mode-verb.js +19 -0
  139. package/dist/core/runner/turn-attachments.d.ts +3 -3
  140. package/dist/core/runner/turn-attachments.js +1 -1
  141. package/dist/core/runner/write-protection-station.d.ts +7 -1
  142. package/dist/core/runner/write-protection-station.js +6 -3
  143. package/dist/core/runner-deps.d.ts +31 -34
  144. package/dist/core/runtime-caps.d.ts +12 -3
  145. package/dist/core/safety-axis-vocab.d.ts +2 -2
  146. package/dist/core/safety-axis-vocab.js +1 -1
  147. package/dist/core/secret-env.d.ts +30 -58
  148. package/dist/core/secret-env.js +149 -586
  149. package/dist/core/sensitive-path-policy.d.ts +4 -2
  150. package/dist/core/sensitive-path-policy.js +41 -31
  151. package/dist/core/store-contracts/checkpoint-store-contract.js +2 -1
  152. package/dist/core/task-event.d.ts +1 -1
  153. package/dist/core/task-result.d.ts +1 -1
  154. package/dist/core/task-spec.d.ts +41 -89
  155. package/dist/core/task-stream.d.ts +16 -0
  156. package/dist/core/terminal-cause.js +1 -0
  157. package/dist/core/tighten-task-spec.d.ts +2 -2
  158. package/dist/core/tighten-task-spec.js +12 -14
  159. package/dist/core/tool-catalog-entries.js +4 -3
  160. package/dist/core/tool-policy.d.ts +84 -30
  161. package/dist/core/tool-policy.js +76 -16
  162. package/dist/core/tool-registry.d.ts +38 -0
  163. package/dist/core/tool-registry.js +25 -1
  164. package/dist/core/tool-roster.d.ts +17 -2
  165. package/dist/core/tool-roster.js +11 -1
  166. package/dist/core/tool-spec.d.ts +8 -9
  167. package/dist/core/trace.d.ts +18 -6
  168. package/dist/core/types.d.ts +2 -1
  169. package/dist/core/wiring-manifest.d.ts +31 -5
  170. package/dist/core/wiring-manifest.js +28 -1
  171. package/dist/core/write-protect.d.ts +11 -1
  172. package/dist/core/write-protect.js +26 -15
  173. package/dist/engine/execution-env/node-execution-env.d.ts +6 -1
  174. package/dist/engine/execution-env/node-execution-env.js +16 -3
  175. package/dist/engine/harness/agent-harness.js +2 -1
  176. package/dist/engine/harness/types.d.ts +6 -7
  177. package/dist/engine/llm/types.d.ts +2 -4
  178. package/dist/engine/loop/types.d.ts +7 -0
  179. package/dist/engine/lsp/node-lsp-manager.js +1 -1
  180. package/dist/index.d.ts +18 -8
  181. package/dist/index.js +15 -6
  182. package/dist/orchestration/workflow-governance.js +1 -1
  183. package/dist/orchestration/workflow-primitives.d.ts +1 -1
  184. package/dist/orchestration/workflow-size-guideline.js +2 -2
  185. package/dist/stores/file/index.d.ts +5 -3
  186. package/dist/stores/file/session-store.d.ts +8 -2
  187. package/dist/stores/file/session-store.js +5 -5
  188. package/dist/tools/artifact/artifact-tool.d.ts +17 -0
  189. package/dist/tools/artifact/artifact-tool.js +16 -1
  190. package/dist/tools/fs/bash-readonly-classifier.d.ts +28 -7
  191. package/dist/tools/fs/bash-readonly-classifier.js +88 -0
  192. package/dist/tools/fs/containment-face.d.ts +9 -3
  193. package/dist/tools/fs/containment-face.js +2 -0
  194. package/dist/tools/fs/file-read-check.d.ts +50 -0
  195. package/dist/tools/fs/file-read-check.js +79 -0
  196. package/dist/tools/fs/fs-bash.d.ts +3 -3
  197. package/dist/tools/fs/fs-read.d.ts +1 -0
  198. package/dist/tools/fs/fs-search-tools.d.ts +2 -2
  199. package/dist/tools/fs/fs-search-tools.js +4 -4
  200. package/dist/tools/fs/fs-shared.js +20 -6
  201. package/dist/tools/fs/fs-write.d.ts +4 -3
  202. package/dist/tools/fs/fs-write.js +9 -9
  203. package/dist/tools/fs/index.d.ts +39 -3
  204. package/dist/tools/fs/index.js +38 -15
  205. package/dist/tools/fs/read-deny-home.d.ts +93 -0
  206. package/dist/tools/fs/read-deny-home.js +225 -0
  207. package/dist/tools/fs/read-deny.d.ts +176 -34
  208. package/dist/tools/fs/read-deny.js +268 -61
  209. package/dist/tools/fs/safety.d.ts +6 -1
  210. package/dist/tools/fs/safety.js +18 -5
  211. package/dist/tools/fs/search.d.ts +3 -0
  212. package/dist/tools/fs/search.js +47 -9
  213. package/dist/tools/fs/shell-read-check.d.ts +52 -13
  214. package/dist/tools/fs/shell-read-check.js +199 -20
  215. package/dist/tools/fs/shell-read-establish.d.ts +7 -3
  216. package/dist/tools/fs/shell-read-establish.js +134 -21
  217. package/dist/tools/fs/shell-read-identity.d.ts +7 -0
  218. package/dist/tools/fs/shell-read-identity.js +21 -0
  219. package/dist/tools/fs/shell-read-judge.d.ts +92 -32
  220. package/dist/tools/fs/shell-read-judge.js +43 -18
  221. package/dist/tools/fs/shell-read-plan.d.ts +4 -0
  222. package/dist/tools/fs/shell-read-plan.js +26 -4
  223. package/dist/tools/fs/shell-read-write-targets.d.ts +73 -0
  224. package/dist/tools/fs/shell-read-write-targets.js +173 -0
  225. package/package.json +1 -1
  226. package/test/export-surface.snapshot.json +117 -11
  227. package/wire-changes/core@7.34.0.json +2621 -0
  228. package/wire-changes/core@7.35.0.json +584 -0
@@ -141,6 +141,10 @@ export declare const READ_RULE_TOOL = "Read";
141
141
  * catalog's declarations, exactly as the protective fences' covered set does.
142
142
  */
143
143
  export declare function ruleToolGrammarOf(toolName: string): "command" | "path" | undefined;
144
+ /** The names the persisted grammar judges a call under: every one of the call's rule names (`ruleNamesOf` — the wire
145
+ * name, then the aliases that resolve to it) the grammar speaks for, so a stored `Bash(…)` or `Edit(…)` row reaches a
146
+ * tool that answers to that name under another wire name, as a policy's rule does. Empty ⇒ the grammar speaks for none. */
147
+ export declare function laneToolNamesOf(req: Pick<import("./tool-policy.js").ToolCallRequest, "toolName" | "face">): readonly string[];
144
148
  /**
145
149
  * Match forms. Four members, two of them one predicate:
146
150
  * · `exact` — admits the one string the rule spells.
@@ -558,6 +562,18 @@ export declare function isUsablePathBase(base: string | undefined, family: PathF
558
562
  * no root reached nothing at all).
559
563
  */
560
564
  export declare function pathRuleReachOf(rule: Pick<PersistedRule, "match" | "command" | "behavior">, target: PathIdentity, bases: PathRuleBases): ProgramRunReachOutcome;
565
+ /**
566
+ * Where a path rule's reach STARTS under the call's bases — its HEAD: the location the resolved pattern names before
567
+ * its first segment carrying `*` (the whole path when none does), or a directory rule's directory. Everything the rule
568
+ * reaches lies at or below the head, so a spelling of the head that the environment resolves elsewhere (a link, a
569
+ * firmlink, `/var` → `/private/var`) is a second spelling of everything the rule reaches — the deny tables judge the
570
+ * rule at both ({@link import("../tools/fs/read-deny.js").withRuleRowsWhereTheyLead}; the reference's "physical twin"
571
+ * of a rule prefix, cli280.js 9766583). The head may be the bare anchor (`/`, a drive or share root — `Read(/**)` at a
572
+ * drive root): an environment can resolve a drive or share root elsewhere. `undefined`: the pattern needs a base the
573
+ * call does not supply (the rule then reads `unreadable`, a reach, everywhere), or a directory rule names the bare
574
+ * anchor (it admits nothing).
575
+ */
576
+ export declare function pathRuleHeadOf(rule: Pick<PersistedRule, "match" | "command" | "behavior">, bases: PathRuleBases): PathIdentity | undefined;
561
577
  /**
562
578
  * Does this rule's command pattern admit `command`?
563
579
  *
@@ -793,7 +809,8 @@ export declare function eligiblePersisted(rule: PersistedRule, call: {
793
809
  * found BEFORE any conjunction: reporting one rule is narrower than reporting a set;
794
810
  * 3. the conjunction arm, compounds only: every segment must be admitted by some eligible rule under
795
811
  * the MATCH reading (the allow side's own under-approximating instrument — never the deny side's
796
- * wider PROGRAM_RUNS reading). Per segment the FIRST table-order admitting rule is selected, and
812
+ * wider PROGRAM_RUNS reading), or be a read-only segment that allows itself ({@link readsOnlyAsSegment});
813
+ * at least one rule must speak. Per segment the FIRST table-order admitting rule is selected, and
797
814
  * the coverage set is those selections in segment order, de-duplicated — the same command against
798
815
  * the same rule table always names the same set (reporting-order determinism, the historical
799
816
  * "order is defined only for reporting" contract extended to sets).
@@ -832,8 +849,8 @@ export interface PersistedRuleVerdict {
832
849
  readonly rules: readonly PersistedRule[];
833
850
  /** The call could not be READ against the person's deny/ask rows (see {@link programRunReachOf}):
834
851
  * `behavior` is `ask`, `rules` names the first tightening row that could not be judged, and this is
835
- * the lexer's reason. The lane projects it as an UNREADABLE answer — the same fail-closed shape an
836
- * unreadable store takes — never as a matched ask rule. */
852
+ * the lexer's reason. The lane projects it as an UNREADABLE answer carrying that reason (it fails closed)
853
+ * — never as a matched ask rule. */
837
854
  readonly unreadable?: string;
838
855
  }
839
856
  /**
@@ -4,8 +4,10 @@ import { compileReadDeny } from "../tools/fs/read-deny.js";
4
4
  import { carriesShellRedirection, parseLeadingCommandName, splitShellCompoundSegments } from "../tools/fs/bash-readonly-classifier.js";
5
5
  import { inlineUntrusted } from "./untrusted-text.js";
6
6
  import { pathTargetOf } from "./tool-registry.js";
7
+ import { ruleNamesOf } from "./tool-roster.js";
7
8
  import { carriesDotSegment, isAbsoluteForFamily, isIdentityDir, isPathIdentity, isShellRootedSpellingUnmapped, isUncIdentity, pathFamilyOf, pathIdentityOf } from "../tools/fs/safety.js";
8
9
  import { readShellCommand } from "./shell-lexer.js";
10
+ import { readOnlyLineObjection, readOnlyShellVerdict } from "./read-only-shell.js";
9
11
  export const RULE_BEHAVIORS = ["deny", "ask", "allow"];
10
12
  const RULE_BEHAVIOR_SET = new Set(RULE_BEHAVIORS);
11
13
  export function isRuleBehavior(v) {
@@ -29,6 +31,9 @@ export function ruleToolGrammarOf(toolName) {
29
31
  return "command";
30
32
  return pathTargetOf(toolName) !== undefined ? "path" : undefined;
31
33
  }
34
+ export function laneToolNamesOf(req) {
35
+ return ruleNamesOf(req).filter((n) => ruleToolGrammarOf(n) !== undefined);
36
+ }
32
37
  export const MAX_RULE_TEXT_CHARS = 512;
33
38
  export const BARE_INTERPRETER_NAMES = new Set([
34
39
  "node", "deno", "bun", "python", "python2", "python3", "perl", "ruby", "php", "osascript",
@@ -450,6 +455,29 @@ export function pathRuleReachOf(rule, target, bases) {
450
455
  const segments = target.split("/").filter((sg) => sg !== "");
451
456
  return globSegmentsReach(globs, segments) ? REACHED : NOT_REACHED;
452
457
  }
458
+ export function pathRuleHeadOf(rule, bases) {
459
+ const family = pathFamilyOf(bases);
460
+ const tree = bases.root ?? bases.cwd;
461
+ let head;
462
+ if (rule.match === "subpath") {
463
+ if (carriesDotSegment(rule.command, family))
464
+ return undefined;
465
+ const dir = pathIdentityOf(rule.command, family, tree);
466
+ return dir.ok && isIdentityDir(dir.identity, family) ? dir.identity : undefined;
467
+ }
468
+ else if (rule.match === "path") {
469
+ const r = resolvePathPattern(rule.command, bases, rule.behavior);
470
+ if ("missingBase" in r)
471
+ return undefined;
472
+ const parts = r.resolved.split("/");
473
+ const wild = parts.findIndex((part) => part.includes("*"));
474
+ head = (wild === -1 ? parts : parts.slice(0, wild)).join("/");
475
+ }
476
+ else
477
+ return undefined;
478
+ const id = pathIdentityOf(head === "" ? "/" : head, family, tree);
479
+ return id.ok ? id.identity : undefined;
480
+ }
453
481
  function hasUnescapedStar(s) {
454
482
  for (let i = 0; i < s.length; i++) {
455
483
  if (s[i] !== "*")
@@ -765,15 +793,22 @@ export function findAdmittingRule(rules, call) {
765
793
  return lone !== undefined ? [lone] : undefined;
766
794
  }
767
795
  const covering = [];
796
+ const lineObjection = readOnlyLineObjection(call.command, call.execCwd ?? call.cwd);
768
797
  for (const [i, segment] of shape.segments.entries()) {
769
798
  const dir = cdDirs[i];
770
799
  const first = firstEligibleAdmittingRule(rules, segment, call) ?? (dir !== undefined ? firstEligibleDirectoryRule(rules, dir, call) : undefined);
771
- if (first === undefined)
800
+ if (first === undefined) {
801
+ if (readsOnlyAsSegment(segment, lineObjection))
802
+ continue;
772
803
  return undefined;
804
+ }
773
805
  if (!covering.includes(first))
774
806
  covering.push(first);
775
807
  }
776
- return covering;
808
+ return covering.length > 0 ? covering : undefined;
809
+ }
810
+ function readsOnlyAsSegment(segment, lineObjection) {
811
+ return lineObjection === undefined && !movesShellCursor(segment) && readOnlyShellVerdict(segment).readOnly;
777
812
  }
778
813
  function firstEligibleDirectoryRule(rules, directory, call) {
779
814
  for (const rule of rules) {
@@ -855,11 +890,13 @@ export function segmentCoverageOf(command, rules, call) {
855
890
  return undefined;
856
891
  const proposed = rules.proposed ?? [];
857
892
  const cdDirs = resolveCdSegmentDirectories(shape.segments, call.execCwd ?? call.cwd);
893
+ const lineObjection = readOnlyLineObjection(folded, call.execCwd ?? call.cwd);
858
894
  return shape.segments.map((segment, i) => {
859
895
  const dir = cdDirs[i];
860
896
  return {
861
897
  segment: segment.trim(),
862
- covered: firstEligibleAdmittingRule(rules.persisted, segment, call) !== undefined ||
898
+ covered: readsOnlyAsSegment(segment, lineObjection) ||
899
+ firstEligibleAdmittingRule(rules.persisted, segment, call) !== undefined ||
863
900
  (dir !== undefined && firstEligibleDirectoryRule(rules.persisted, dir, call) !== undefined) ||
864
901
  proposed.some((p) => p.rule.tool === "Read"
865
902
  ? dir !== undefined &&
@@ -72,12 +72,16 @@ export interface EffectivePermissionRules {
72
72
  org: OrgRuleResolution;
73
73
  /** The durable partition's revision — the optimistic-concurrency anchor for its writes. */
74
74
  rev: number;
75
- /** Present iff the DURABLE partition could not be read: `rules`/`removed` are then empty and `rev` is 0,
76
- * while the org partition's resolution beside it still stands (a flaky personal store must not become
77
- * an org governance outage). A consumer that needs the durable rows treats this as a read failure —
78
- * {@link effectiveOrThrow} is that reading; the gate's personal lane reads it as "unreadable"
79
- * (zero rules + a disclosure, fail toward asking). */
75
+ /** Present iff a PERSONAL partition could not be read — the durable one (its user / project rows; `removed` is then
76
+ * empty and `rev` 0) or the session one: the failed partition's rows are absent from `rules`, while the rows of the
77
+ * partition that WAS read stand, and so does the org resolution beside them (a flaky personal store must not become an
78
+ * org governance outage). The line names the failure (both lines, `; `-joined, when both failed). A consumer that needs
79
+ * the durable rows treats this as a read failure — {@link effectiveOrThrow} is that reading; the gate's personal lane
80
+ * and the leg's path-row read skip the failed partition's rows, disclosed, and judge by the rest. */
80
81
  unreadable?: string;
82
+ /** Beside {@link unreadable}: the sources whose partition could not be read (`user` and `project` — the durable
83
+ * partition; `session`), in that order — the rows `rules` does not carry. */
84
+ unreadableSources?: readonly Exclude<RuleSource, "org">[];
81
85
  /** Disclosure lines from the personal partitions (a session partition that could not be read, rows it
82
86
  * served that are not canonical session rows of this session). The org partition's lines ride
83
87
  * `org.disclosures`. */
@@ -85,12 +89,11 @@ export interface EffectivePermissionRules {
85
89
  }
86
90
  /**
87
91
  * The host-visible store, already anchored to one owner. One read verb, which never throws: each
88
- * partition reports its own failure in the view — the DURABLE partition's read failure as `unreadable`
89
- * (zero rows; the caller maps it to "unreadable": a loud disclosure, fail toward asking, exactly as the
90
- * old `list()` throw did), the SESSION partition's read failure as `unreadable` too (it may hold a
91
- * deny/ask row of this session, and zero rows would erase it — served-but-non-canonical rows stay a
92
- * `disclosures` line), the ORG partition's as its `status`. The org verdict stands while a personal
93
- * partition is down, and the personal rows stand while an org source is down.
92
+ * partition reports its own failure in the view — a PERSONAL partition's read failure (the durable one's, the
93
+ * session one's) as `unreadable` / `unreadableSources`, with that partition's rows absent and the other's present
94
+ * (served-but-non-canonical session rows stay a `disclosures` line), the ORG partition's as its `status`. The unit
95
+ * that fails is the partition: the org verdict stands while a personal partition is down, each personal partition's
96
+ * rows stand while the other is down, and the personal rows stand while an org source is down.
94
97
  */
95
98
  export interface PermissionRuleStore {
96
99
  effective(query?: {
@@ -119,6 +122,14 @@ export interface PermissionRuleStoreProvider {
119
122
  session: boolean;
120
123
  }>;
121
124
  }
125
+ /**
126
+ * The store one task reads its person's rules from. The unauthenticated floor is enforced HERE, not merely asked of the
127
+ * provider: an absent principal resolves to zero personal rules, and a provider that forgot it would hand one person's
128
+ * standing approvals to an anonymous task. The ONE exception is an explicitly declared LOCAL-OWNER deployment, where "no
129
+ * principal" is the typed absence of identity — the machine owner's own bucket (a provider with no such face resolves
130
+ * the anonymous store; {@link createPermissionRuleLanes} refuses that declaration loudly).
131
+ */
132
+ export declare function ownerStoreOf(provider: PermissionRuleStoreProvider, principal: string | undefined, localOwnerDeclared: boolean): PermissionRuleStore;
122
133
  /** What {@link createPermissionRuleStoreProvider} composes. */
123
134
  export interface PermissionRuleStoreConfig {
124
135
  /** The durable CRDT partition (user + project sources). Absent ⇒ no personal rules exist on this
@@ -4,6 +4,10 @@ import { UNGOVERNED_ORG_RESOLUTION, compileOrgShadowPredicate, createOrgRulePart
4
4
  export function ruleSourceOf(scope) {
5
5
  return scope.kind === "global" ? "user" : scope.kind;
6
6
  }
7
+ export function ownerStoreOf(provider, principal, localOwnerDeclared) {
8
+ const anonymous = principal === undefined || principal === "";
9
+ return anonymous && localOwnerDeclared && provider.forLocalOwner !== undefined ? provider.forLocalOwner() : provider.forPrincipal(anonymous ? undefined : principal);
10
+ }
7
11
  function usableSessionId(sessionId) {
8
12
  return typeof sessionId === "string" && sessionId !== "" ? sessionId : undefined;
9
13
  }
@@ -61,16 +65,22 @@ class UnifiedPermissionRuleStore {
61
65
  this.readSessionRows(usableSessionId(query?.sessionId), disclosures),
62
66
  ]);
63
67
  const orgResolution = orgSettled;
64
- if ("unreadable" in sessionRows) {
65
- return { rules: [], removed: [], org: orgResolution, rev: 0, disclosures, unreadable: sessionRows.unreadable };
66
- }
68
+ const unreadable = [];
69
+ const unreadableSources = [];
67
70
  if (!durableSettled.ok) {
68
- return { rules: [], removed: [], org: orgResolution, rev: 0, disclosures, unreadable: errText(durableSettled.e) };
71
+ unreadable.push(errText(durableSettled.e));
72
+ unreadableSources.push("user", "project");
73
+ }
74
+ if ("unreadable" in sessionRows) {
75
+ unreadable.push(sessionRows.unreadable);
76
+ unreadableSources.push("session");
69
77
  }
70
- const durableRead = durableSettled.v;
78
+ const failed = unreadable.length > 0 ? { unreadable: unreadable.join("; "), unreadableSources } : {};
79
+ const durableRead = durableSettled.ok ? durableSettled.v : EMPTY_DURABLE;
80
+ const sessionRead = "unreadable" in sessionRows ? [] : sessionRows;
71
81
  const shadowed = compileOrgShadowPredicate(orgResolution.rules);
72
82
  const rules = [];
73
- for (const r of [...sessionRows, ...durableRead.rules]) {
83
+ for (const r of [...sessionRead, ...durableRead.rules]) {
74
84
  rules.push({ ...r, source: ruleSourceOf(r.scope), status: r.behavior === "allow" && shadowed(r) ? "shadowed-by-org" : "live" });
75
85
  }
76
86
  const removed = [];
@@ -83,7 +93,7 @@ class UnifiedPermissionRuleStore {
83
93
  continue;
84
94
  removed.push({ behavior: t.behavior, rule: t.rule, scope: t.scope, source: ruleSourceOf(t.scope), status: "removed" });
85
95
  }
86
- return { rules, removed, org: orgResolution, rev: durableRead.rev, disclosures };
96
+ return { rules, removed, org: orgResolution, rev: durableRead.rev, disclosures, ...failed };
87
97
  }
88
98
  async readSessionRows(sessionId, disclosures) {
89
99
  if (this.session === undefined || sessionId === undefined)
@@ -47,7 +47,8 @@
47
47
  */
48
48
  import type { NamedToolPolicy } from "./tool-policy.js";
49
49
  import { parsePermissionRule, type ParsedPermissionRule } from "./permission-rule-syntax.js";
50
- import { type PathRuleBases } from "./permission-rule-model.js";
50
+ import { type ParsedRule, type PathRuleBases } from "./permission-rule-model.js";
51
+ import type { DenyFace, PathRuleDenyRow } from "../tools/fs/read-deny.js";
51
52
  export { parsePermissionRule, type ParsedPermissionRule };
52
53
  /** [K-PLATFORM-SWEEP] MINOR#11: Bash's REAL non-primary arg names, accepted as generic param rules
53
54
  * (`Bash(timeout:...)`). Exported SOLELY so a lockstep test can assert it against the live Bash tool
@@ -190,6 +191,27 @@ export interface RuleFaceView {
190
191
  export declare function catalogRuleFaceView(toolName: string): RuleFaceView | undefined;
191
192
  /** Exported for the lockstep guard only (the scalar vocabulary of a catalog tool) — not part of the rule DSL. */
192
193
  export declare const fileToolParamVocabulary: (canonicalTool: string) => ReadonlySet<string> | undefined;
194
+ /**
195
+ * The path DENY rules that speak for a FACE rather than for one tool (the reference implementation's reading: a Read
196
+ * deny rule covers every built-in tool that reads a file and the shell's file commands; an Edit deny rule every tool
197
+ * that edits one; a Read deny also refuses writing the same path). Such a rule reaches any call whose declared path
198
+ * target is on one of its faces — the write tools, the read tools, the search roots — and it is exported to the
199
+ * engine's deny tables (`ToolPolicy.pathDenyRules`) so the shell's reads and writes and the
200
+ * traversals meet it too. A path rule on any other tool keeps its tool's name as its reach (`Write(<path>)` is judged on
201
+ * the Write tool, a stricter reading the reference does not make).
202
+ */
203
+ export declare const PATH_DENY_RULE_FACES: Readonly<Record<string, readonly DenyFace[]>>;
204
+ /**
205
+ * The deny-table row of ONE face-keyed path deny rule, resolved against `bases` — the row's reach IS the rule's (the same
206
+ * parsed rule, the same bases). Every source of such rows builds them here: a policy's compiled rules, and the person's
207
+ * persisted store rows. The caller holds the bases the pattern needs (`ruleBasesNeeded`): a row whose base is missing
208
+ * reads `unreadable`, which this row treats as a reach — a deny row that cannot be judged holds.
209
+ */
210
+ export declare function pathRuleDenyRowOf(rule: {
211
+ readonly parsed: ParsedRule;
212
+ readonly ruleText: string;
213
+ readonly faces: readonly DenyFace[];
214
+ }, bases: PathRuleBases): PathRuleDenyRow;
193
215
  /** Dry-run 校验/迁移报告:不 throw,返回全部 issues(含 `unsupported.*` 分类,
194
216
  * 供从 CC settings 迁移的部署逐条对照)。 */
195
217
  export declare function validatePermissionRules(rules: PermissionRule[], opts?: Pick<PermissionRulePolicyOptions, "primaryFieldGeneric" | "caps" | "ruleFaces" | "pathBases">): PermissionRuleIssue[];
@@ -1,11 +1,12 @@
1
1
  import { MCP_NAMESPACE, protocolOf } from "./protocol-table.js";
2
2
  import { catalogRuleFaceOf, pathTargetOf } from "./tool-registry.js";
3
3
  import { indexOfUnescaped, lastIndexOfUnescaped, parsePermissionRule } from "./permission-rule-syntax.js";
4
- import { PATH_RULE_BASE_LABEL, isUsablePathBase, parseRuleText, pathRuleReachOf, programRunReachOf, ruleBasesNeeded } from "./permission-rule-model.js";
5
- import { pathFamilyOf } from "../tools/fs/safety.js";
4
+ import { PATH_RULE_BASE_LABEL, READ_RULE_TOOL, isUsablePathBase, parseRuleText, pathRuleHeadOf, pathRuleReachOf, programRunReachOf, ruleBasesNeeded } from "./permission-rule-model.js";
5
+ import { pathFamilyOf, pathIdentityOf, spellingOfIdentity } from "../tools/fs/safety.js";
6
6
  import { effectivePathTargetOf } from "./effective-path-target.js";
7
7
  import { protectivePathTargetOf } from "./tool-registry.js";
8
8
  import { ruleDenialSentence } from "./rule-denial-sentence.js";
9
+ import { ruleNamesOf } from "./tool-roster.js";
9
10
  export { parsePermissionRule };
10
11
  export const BASH_GENERIC_PARAMS = new Set(catalogRuleFaceOf("Bash")?.params ?? []);
11
12
  const DEFAULT_CAPS = {
@@ -81,6 +82,27 @@ export const fileToolParamVocabulary = (canonicalTool) => {
81
82
  const face = catalogRuleFaceOf(canonicalTool);
82
83
  return face?.params === undefined || pathTargetOf(canonicalTool) === undefined ? undefined : new Set(face.params);
83
84
  };
85
+ export const PATH_DENY_RULE_FACES = Object.freeze({ [READ_RULE_TOOL]: ["read", "write"], Edit: ["write"] });
86
+ export function pathRuleDenyRowOf(rule, bases) {
87
+ const family = pathFamilyOf(bases);
88
+ const tree = bases.root ?? bases.cwd;
89
+ const head = pathRuleHeadOf(rule.parsed, bases);
90
+ return {
91
+ rule: rule.ruleText,
92
+ tool: rule.parsed.tool,
93
+ pattern: parsePermissionRule(rule.ruleText).ruleContent ?? "",
94
+ faces: rule.faces,
95
+ reaches: (absolutePath) => {
96
+ const id = pathIdentityOf(absolutePath, family, tree);
97
+ return id.ok && pathRuleReachOf(rule.parsed, id.identity, bases).reach !== "not_reached";
98
+ },
99
+ ...(head !== undefined ? { head: spellingOfIdentity(head, family) } : {}),
100
+ };
101
+ }
102
+ function callPathFaceOf(req) {
103
+ const target = protectivePathTargetOf(req);
104
+ return target === undefined ? undefined : target.access === "read" ? "read" : "write";
105
+ }
84
106
  function countStars(s) {
85
107
  let n = 0;
86
108
  for (const c of s)
@@ -196,7 +218,8 @@ function compile(rules, caps, primaryFieldGeneric, ruleFaces, pathBases = {}) {
196
218
  }
197
219
  if (issues.length > 0 && issues[issues.length - 1].rule === text && issues[issues.length - 1].code === "unsupported.path_base")
198
220
  continue;
199
- entry.content[r.behavior].push({ parsed: parsedPath.rule, ruleText: text, ...(r.source !== undefined ? { source: r.source } : {}) });
221
+ const faces = r.behavior === "deny" ? PATH_DENY_RULE_FACES[toolName] : undefined;
222
+ entry.content[r.behavior].push({ parsed: parsedPath.rule, ruleText: text, ...(r.source !== undefined ? { source: r.source } : {}), ...(faces !== undefined ? { faces } : {}) });
200
223
  byTool.set(toolName, entry);
201
224
  continue;
202
225
  }
@@ -317,6 +340,8 @@ export function createPermissionRulePolicy(rules, opts) {
317
340
  }
318
341
  const coveringHit = (lane, toolName) => lane.length === 0 ? undefined : lane.find((r) => namespacedRuleNameCovers(r.ruleName, toolName));
319
342
  const hasCovering = namespacedCovering.deny.length > 0 || namespacedCovering.ask.length > 0 || namespacedCovering.allow.length > 0;
343
+ const faceDenyRules = [...byTool.values()].flatMap((e) => e.content.deny.filter((r) => r.faces !== undefined));
344
+ const pathDenyRules = faceDenyRules.map((r) => pathRuleDenyRowOf({ parsed: r.parsed, ruleText: r.ruleText, faces: r.faces }, pathBases));
320
345
  const commandParam = catalogRuleFaceOf("Bash")?.primaryParams[0] ?? "command";
321
346
  const contentHit = (lane, req) => {
322
347
  if (lane.length === 0)
@@ -349,16 +374,36 @@ export function createPermissionRulePolicy(rules, opts) {
349
374
  }
350
375
  return unreadable;
351
376
  };
352
- const unreadableAsk = (hit, req) => ({
377
+ const unreadableAsk = (hit, lane, req) => ({
353
378
  action: "ask",
354
379
  message: `tool "${req.toolName}" needs approval: the call could not be read against the permission rule ${hit.rule.ruleText} (${hit.unreadable}) — a person must decide`,
355
- matchedAskRule: hit.rule.ruleText,
380
+ ruleUnreadable: lane,
356
381
  requiresRealApproval: true,
357
382
  });
383
+ const entryFor = (req) => {
384
+ const entries = ruleNamesOf(req).flatMap((n) => {
385
+ const e = byTool.get(n);
386
+ return e !== undefined ? [e] : [];
387
+ });
388
+ if (entries.length <= 1)
389
+ return entries[0];
390
+ const bare = {};
391
+ for (const b of ["deny", "ask", "allow"]) {
392
+ const hit = entries.find((e) => e.bare[b] !== undefined)?.bare[b];
393
+ if (hit !== undefined)
394
+ bare[b] = hit;
395
+ }
396
+ return {
397
+ bare,
398
+ param: { deny: entries.flatMap((e) => e.param.deny), ask: entries.flatMap((e) => e.param.ask) },
399
+ content: { deny: entries.flatMap((e) => e.content.deny), ask: entries.flatMap((e) => e.content.ask) },
400
+ };
401
+ };
358
402
  return {
359
403
  nameSets: [nameSets],
404
+ ...(pathDenyRules.length > 0 ? { pathDenyRules } : {}),
360
405
  check(req) {
361
- const entry = byTool.get(req.toolName);
406
+ const entry = entryFor(req);
362
407
  const covering = hasCovering && protocolOf(req.toolName) !== undefined;
363
408
  if (entry?.bare.deny) {
364
409
  return { action: "deny", message: ruleDenial(req, entry.bare.deny.ruleText, entry.bare.deny.source) };
@@ -368,7 +413,9 @@ export function createPermissionRulePolicy(rules, opts) {
368
413
  if (coveringDeny)
369
414
  return { action: "deny", message: ruleDenial(req, coveringDeny.ruleText, coveringDeny.source) };
370
415
  }
371
- const contentDeny = entry !== undefined ? contentHit(entry.content.deny, req) : undefined;
416
+ const callFace = faceDenyRules.length > 0 ? callPathFaceOf(req) : undefined;
417
+ const denyLane = [...(entry?.content.deny.filter((r) => r.faces === undefined) ?? []), ...(callFace !== undefined ? faceDenyRules.filter((r) => r.faces.includes(callFace)) : [])];
418
+ const contentDeny = denyLane.length > 0 ? contentHit(denyLane, req) : undefined;
372
419
  if (contentDeny !== undefined && contentDeny.unreadable === undefined) {
373
420
  return { action: "deny", message: ruleDenial(req, contentDeny.rule.ruleText, contentDeny.rule.source) };
374
421
  }
@@ -379,7 +426,7 @@ export function createPermissionRulePolicy(rules, opts) {
379
426
  }
380
427
  }
381
428
  if (contentDeny?.unreadable !== undefined)
382
- return unreadableAsk({ rule: contentDeny.rule, unreadable: contentDeny.unreadable }, req);
429
+ return unreadableAsk({ rule: contentDeny.rule, unreadable: contentDeny.unreadable }, "content_deny", req);
383
430
  if (entry) {
384
431
  if (entry.bare.ask) {
385
432
  return { action: "ask", message: ruleAskMessage(entry.bare.ask.ruleText, entry.bare.ask.source), matchedAskRule: entry.bare.ask.ruleText };
@@ -394,7 +441,7 @@ export function createPermissionRulePolicy(rules, opts) {
394
441
  const contentAsk = contentHit(entry.content.ask, req);
395
442
  if (contentAsk !== undefined) {
396
443
  if (contentAsk.unreadable !== undefined)
397
- return unreadableAsk({ rule: contentAsk.rule, unreadable: contentAsk.unreadable }, req);
444
+ return unreadableAsk({ rule: contentAsk.rule, unreadable: contentAsk.unreadable }, "content_ask", req);
398
445
  return { action: "ask", message: ruleAskMessage(contentAsk.rule.ruleText, contentAsk.rule.source), matchedAskRule: contentAsk.rule.ruleText };
399
446
  }
400
447
  const paramAsk = matchParamRules(entry.param.ask, req.args, caps.maxScalarValueChars);
@@ -12,16 +12,20 @@
12
12
  * (mandates, the org latch, the marked-call seat) rather than a function of the answer alone.
13
13
  */
14
14
  import type { PermissionResult } from "./tool-policy.js";
15
- import type { PersistedRuleHit, PersistedRuleHitRule } from "./hooks.js";
15
+ import type { PersistedRuleAnswer, PersistedRuleHit, PersistedRuleHitRule } from "./hooks.js";
16
16
  /** The lane's one read, normalized: at most one of `hit` / `unreadable` / `coverage` (the gate's reader
17
17
  * produces it; the arms below read it). */
18
18
  export interface PersistedRuleRead {
19
19
  readonly hit?: PersistedRuleHit;
20
+ /** The lane could not answer: with `reason`, the CALL could not be read against the person's deny/ask rows;
21
+ * without, the store could not be read — its rows take no part in the decision (the record says so). */
20
22
  readonly unreadable?: true;
21
- /** The lexer's reason when the CALL could not be read against the person's deny/ask rows (absent for
22
- * a store read failure) — the message says which it was; the decision is the same. */
23
+ /** The lexer's reason when the CALL could not be read against the person's deny/ask rows. */
23
24
  readonly reason?: string;
24
25
  }
26
+ /** The answers of one store read judged under each of a call's names, folded as one table folds its rows (deny >
27
+ * unreadable > ask > allow > coverage > nothing); among equals the first (the wire name's) stands. */
28
+ export declare function strongestAnswer(answers: readonly PersistedRuleAnswer[]): PersistedRuleAnswer;
25
29
  /** Display form of a rule set (design/375 §5.1): every rule text is a DISPLAY value on a human trust
26
30
  * boundary, and the set must stay readable however large a lane makes it — each member rides
27
31
  * `inlineUntrusted` (200 cap, the same cap the durable leg's `RiskDescriptor.shadowedRule` carries),
@@ -45,14 +49,14 @@ export declare function disclosedRuleSet(rules: readonly PersistedRuleHitRule[])
45
49
  * ask, the marker is stamped if absent and the message names the rule — the standing question
46
50
  * outranks a remembered yes across lanes, exactly as the DSL's own order says within one; a deny
47
51
  * stays a deny.
48
- * · UNREADABLE — the wired store could not be read (a read failure, a timeout), or the CALL could not be
49
- * read against the person's standing deny/ask rows (`reason` carries the lexer's word: an expansion
50
- * where a rule reads a word, an unterminated quote, a syntax error): the person's deny/ask rows
51
- * cannot be enforced for this call, so it fails CLOSED the way an unreadable org snapshot does — an
52
- * allow tightens to an ask carrying `requiresRealApproval` (no automatic lane clears it; the gate
52
+ * · UNREADABLE CALL — the store was read, but the CALL could not be read against the person's standing
53
+ * deny/ask rows (`reason` carries the lexer's word: an expansion where a rule reads a word, an
54
+ * unterminated quote, a syntax error): whether a row forbids this call is unknown, so it fails CLOSED —
55
+ * an allow tightens to an ask carrying `requiresRealApproval` (no automatic lane clears it; the gate
53
56
  * derives the origin `rule_store_unavailable` from its own record of the read), an ask acquires the
54
- * bit, a deny stays a deny. Reading an unreadable store as "no rule" would erase every deny the
55
- * person wrote the moment their file is locked or corrupt.
57
+ * bit, a deny stays a deny.
58
+ * A store that could not be READ (`unreadable` without a `reason`) contributes no row: the decision is returned
59
+ * untouched, as for no hit — the lane announces the skipped store, and the rows apply again once a read succeeds.
56
60
  * An allow hit (or no hit) returns the decision untouched: the allow arm is the station's.
57
61
  */
58
62
  export declare function applyPersistedTightening(decision: PermissionResult, read: PersistedRuleRead | undefined): {
@@ -1,20 +1,27 @@
1
+ import { decisionAskClassesOf } from "./ask-origin.js";
1
2
  import { inlineUntrusted } from "./untrusted-text.js";
3
+ export function strongestAnswer(answers) {
4
+ const rank = (a) => (a === undefined ? 0 : "unreadable" in a ? 4 : "behavior" in a ? { deny: 5, ask: 3, allow: 2 }[a.behavior] : 1);
5
+ return answers.reduce((best, a) => (rank(a) > rank(best) ? a : best), undefined);
6
+ }
2
7
  export function disclosedRuleSet(rules) {
3
8
  const shown = rules.slice(0, 5).map((r) => inlineUntrusted(r.rule, 200));
4
9
  const rest = rules.length - shown.length;
5
10
  return rest > 0 ? `${shown.join(", ")} and ${rest} more` : shown.join(", ");
6
11
  }
7
12
  export function applyPersistedTightening(decision, read) {
8
- if (read?.unreadable === true) {
13
+ if (read?.unreadable === true && read.reason !== undefined) {
9
14
  if (decision.action === "deny")
10
15
  return { decision };
11
- const message = read.reason !== undefined
12
- ? `this call could not be read against the person's standing deny/ask rules (${read.reason}) — whether one of them forbids it is unknown, so it needs a real approval`
13
- : "the persisted permission-rule store could not be read for this call — the person's own deny/ask rules cannot be enforced, so this call needs a real approval until the store is readable";
16
+ const message = `this call could not be read against the person's standing deny/ask rules (${read.reason}) — whether one of them forbids it is unknown, so it needs a real approval`;
17
+ const unreadableCall = { ruleUnreadable: "persisted_call" };
18
+ const standingFence = decision.action === "ask" && decisionAskClassesOf({ ...decision, foldedAskClasses: undefined }).has("policy_rra")
19
+ ? { foldedAskClasses: [...(decision.foldedAskClasses ?? []), "policy_rra"] }
20
+ : {};
14
21
  return {
15
22
  decision: decision.action === "allow"
16
- ? { action: "ask", message, decisionReason: "persisted_rule", requiresRealApproval: true, ...(decision.updatedInput !== undefined ? { updatedInput: decision.updatedInput } : {}) }
17
- : { ...decision, requiresRealApproval: true, message: `${decision.message !== undefined ? `${decision.message} ` : ""}(${message})` },
23
+ ? { action: "ask", message, decisionReason: "persisted_rule", requiresRealApproval: true, ...unreadableCall, ...(decision.updatedInput !== undefined ? { updatedInput: decision.updatedInput } : {}) }
24
+ : { ...decision, requiresRealApproval: true, ...unreadableCall, ...standingFence, message: `${decision.message !== undefined ? `${decision.message} ` : ""}(${message})` },
18
25
  tightened: "unreadable",
19
26
  };
20
27
  }
@@ -13,7 +13,7 @@ export declare const ENTER_PLAN_MODE_TOOL_NAME = "EnterPlanMode";
13
13
  * making enter execute (set the flag) BEFORE the sibling edit is prepared+gated — closing the same-batch window.
14
14
  * Exit is via `present_plan` (→ plan_review → human approve-resume), not by this tool.
15
15
  */
16
- export declare function createEnterPlanModeTool(enterPlanMode: () => void): ToolSpec;
16
+ export declare function createEnterPlanModeTool(enterPlanMode: () => Promise<void> | void): ToolSpec;
17
17
  /**
18
18
  * design/80 D-B — the first-party `present_plan` tool (CC `ExitPlanMode` parity). A THIN composition over the
19
19
  * general `ctx.requestReview()` primitive: the model calls it with a plan; the tool returns the plan as its
@@ -3,7 +3,7 @@ import { delimitUntrusted } from "./untrusted-text.js";
3
3
  import { toolFace } from "./tool-catalog-entries.js";
4
4
  export const PRESENT_PLAN_TOOL_NAME = "ExitPlanMode";
5
5
  export const ENTER_PLAN_MODE_TOOL_NAME = "EnterPlanMode";
6
- const ENTER_PLAN_MODE_DESCRIPTION = `Enter PLAN MODE (read-only): stop making changes and switch to exploration/planning. For the rest of this run, write/edit/mutating tool calls are REJECTED while read-only tools (Read, Grep, Glob, etc.) still work. When your plan is ready, call ${PRESENT_PLAN_TOOL_NAME} with it — the run PAUSES for a human to approve it, then continues (read-only lifted) so you can execute.
6
+ const ENTER_PLAN_MODE_DESCRIPTION = `Enter PLAN MODE (read-only): stop making changes and switch to exploration/planning. For the rest of this run, every write/edit/mutating tool call ASKS the user first ("Cannot write to <path> while in plan mode."; where nobody can answer, it is refused) while read-only tools (Read, Grep, Glob, etc.) still work. When your plan is ready, call ${PRESENT_PLAN_TOOL_NAME} with it — the run PAUSES for a human to approve it, then continues (read-only lifted) so you can execute.
7
7
 
8
8
  Use this tool proactively when you're about to start a non-trivial implementation task. If you would use AskUserQuestion to clarify the approach, use EnterPlanMode instead — getting alignment on the plan itself is the point.
9
9
 
@@ -37,8 +37,8 @@ export function createEnterPlanModeTool(enterPlanMode) {
37
37
  parameters: Type.Object({}),
38
38
  executionMode: "sequential",
39
39
  execute: async () => {
40
- enterPlanMode();
41
- const message = "Entered plan mode (read-only). Write/edit/mutating tools are now rejected for the rest of this run; " +
40
+ await enterPlanMode();
41
+ const message = "Entered plan mode (read-only). Every write/edit/mutating tool call now asks the user first for the rest of this run; " +
42
42
  `research with read-only tools, then call ${PRESENT_PLAN_TOOL_NAME} with your plan to get it approved before acting.`;
43
43
  return {
44
44
  content: message,
@@ -1,19 +1,3 @@
1
- /**
2
- * The READ-ONLY shell reader — "does this command line provably only READ?" — the allow-layer member
3
- * the tool gate consults for the shell tool after every tightening lane and after the person's own allow
4
- * rules, and before the classifier or a person is asked (`gate-lanes.ts`). Upstream (CC 2.1.250) has the
5
- * same station: its shell tool's `isReadOnly` runs the command through a parser, splits it into its
6
- * simple commands, and admits the whole line only when EVERY simple command is read-only by its tables;
7
- * this module is that reading over this engine's own lexer ({@link readShellCommand}, the tightening
8
- * reader's) and the transcribed tables (`read-only-shell-table.ts`).
9
- *
10
- * The reading itself is the contract on {@link readOnlyShellVerdict}.
11
- *
12
- * What the verdict is NOT: a security boundary of its own. The deny/ask lanes run before it and win; the
13
- * mandated asks (an operator's per-call shell confirmation, a tool's own egress/irreversibility marks, an
14
- * explicit ask rule, a hook's ask, governance) are never cleared by it — the gate's allow-layer predicate
15
- * decides where it may speak, exactly as it decides for a person's stored allow rule.
16
- */
17
1
  import { type ShellCommandShape } from "./shell-lexer.js";
18
2
  /** The reader's answer: read-only, or not — with the first reason the reading stopped on (a sentence a
19
3
  * trace or a card can show; never adjudication input). */
@@ -23,6 +7,14 @@ export type ReadOnlyShellVerdict = {
23
7
  readonly readOnly: false;
24
8
  readonly reason: string;
25
9
  };
10
+ /** The reference implementation's question for a compound that changes directory and runs git (2.1.280, `cd-git-compound`). */
11
+ export declare const CD_GIT_COMPOUND_SENTENCE = "This command changes directory before running a version-control command, which can pick up untrusted hooks or repository configuration from the target directory. Approve only if you trust it.";
12
+ /**
13
+ * Does this line change directory and run git — a `cd` beside a `git` — other than by ONE plain `cd <path>` that lands
14
+ * where the session stands (`cwd`)? The reference's no-op reading, lexical here: the path is `.` / `..`, starts `./` /
15
+ * `../` / `/`, carries no expansion, quote-removed text only, and resolves to `cwd` itself. No `cwd` ⇒ no no-op.
16
+ */
17
+ export declare function changesDirectoryBeforeGit(shape: ShellCommandShape, cwd?: string): boolean;
26
18
  /**
27
19
  * Read `command` as the allow layer must: read-only, or the first reason it is not. A shape already read by
28
20
  * the caller may be handed in (the gate reads every shell command once).
@@ -36,7 +28,15 @@ export type ReadOnlyShellVerdict = {
36
28
  * admitted only under {@link READ_ONLY_GLOB_PROGRAMS}; else the structural reading (the bare-program set,
37
29
  * the exact forms, `echo`/`ls`/`cd`/`find`/`printf`/`history`/`arch`/`ifconfig`); else the flag-vetted
38
30
  * table with its per-row danger predicates, and the `uniq`/`jq` forms. A `cd` beside a `git` in one line
39
- * is never read-only (the git command's repository is then not the session's). Anything the reading does
40
- * not positively admit is NOT read-only — the answer is never a guess, and a `false` costs one question.
31
+ * is never read-only (the git command's repository is then not the session's) unless the `cd` lands where the session
32
+ * stands ({@link changesDirectoryBeforeGit}). Anything the reading does not positively admit is NOT read-only — the
33
+ * answer is never a guess, and a `false` costs one question.
34
+ */
35
+ export declare function readOnlyShellVerdict(command: string, shape?: ShellCommandShape, cwd?: string): ReadOnlyShellVerdict;
36
+ /**
37
+ * The reader's LINE-level objection to `command` — what makes no segment of it read-only in its place, whatever the
38
+ * segment reads on its own: the line's structure, or a `cd` beside a `git` ({@link changesDirectoryBeforeGit}). A caller
39
+ * that judges a compound's segments one by one ({@link readOnlyShellVerdict} over a segment's text) asks this of the
40
+ * line first.
41
41
  */
42
- export declare function readOnlyShellVerdict(command: string, shape?: ShellCommandShape): ReadOnlyShellVerdict;
42
+ export declare function readOnlyLineObjection(command: string, cwd?: string): string | undefined;