@sema-agent/core 5.57.0 → 5.59.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 (192) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/dist/agents/cascade.d.ts +1 -1
  3. package/dist/agents/cumulative-stats.d.ts +1 -1
  4. package/dist/agents/observer.d.ts +2 -2
  5. package/dist/agents/peer-admission.d.ts +1 -1
  6. package/dist/agents/retain-ledger.d.ts +2 -2
  7. package/dist/agents/roster-store.d.ts +8 -8
  8. package/dist/agents/send-message-tool.d.ts +2 -2
  9. package/dist/agents/subagent-steps.d.ts +1 -1
  10. package/dist/agents/subagent.d.ts +13 -13
  11. package/dist/agents/team.d.ts +5 -5
  12. package/dist/agents/tool-filter.d.ts +2 -2
  13. package/dist/agents/verify.d.ts +1 -1
  14. package/dist/bench/metrics.d.ts +35 -35
  15. package/dist/brain/degrading.d.ts +1 -1
  16. package/dist/brain/errors.d.ts +3 -3
  17. package/dist/brain/reasoning.d.ts +2 -2
  18. package/dist/brain/repetition.d.ts +1 -1
  19. package/dist/brain/route-adjudicator.d.ts +8 -1
  20. package/dist/brain/route-adjudicator.js +8 -1
  21. package/dist/brain/status-sink.d.ts +2 -2
  22. package/dist/brain/stream-shared.d.ts +1 -1
  23. package/dist/config/catalog.d.ts +5 -5
  24. package/dist/core/arg-summary.d.ts +4 -4
  25. package/dist/core/ask-class.d.ts +2 -2
  26. package/dist/core/ask-question.d.ts +1 -1
  27. package/dist/core/auto-compaction.d.ts +15 -15
  28. package/dist/core/auto-mode.d.ts +5 -5
  29. package/dist/core/background-agent-store.d.ts +20 -20
  30. package/dist/core/background-shell.d.ts +4 -4
  31. package/dist/core/checkpoint-store.d.ts +35 -27
  32. package/dist/core/context-edit.d.ts +1 -1
  33. package/dist/core/context-guard.d.ts +1 -1
  34. package/dist/core/exec-output-tail.d.ts +6 -6
  35. package/dist/core/file-snapshot-store.d.ts +8 -8
  36. package/dist/core/git-worktree-env.d.ts +3 -3
  37. package/dist/core/governance-codes.d.ts +37 -10
  38. package/dist/core/governance-codes.js +54 -1
  39. package/dist/core/hooks.d.ts +73 -33
  40. package/dist/core/hooks.js +87 -25
  41. package/dist/core/image-downsample.d.ts +1 -1
  42. package/dist/core/locked-config.d.ts +1 -1
  43. package/dist/core/lsp.d.ts +1 -1
  44. package/dist/core/mailbox-store.d.ts +1 -1
  45. package/dist/core/mcp.d.ts +3 -3
  46. package/dist/core/memory-engine/consolidation-driver.d.ts +211 -0
  47. package/dist/core/memory-engine/consolidation-driver.js +427 -0
  48. package/dist/core/memory-engine/consolidation.d.ts +119 -3
  49. package/dist/core/memory-engine/consolidation.js +22 -1
  50. package/dist/core/memory-engine/data-plane.d.ts +1 -1
  51. package/dist/core/memory-engine/distiller.d.ts +550 -0
  52. package/dist/core/memory-engine/distiller.js +598 -0
  53. package/dist/core/memory-engine/dual-root.d.ts +1 -1
  54. package/dist/core/memory-engine/engine.d.ts +143 -10
  55. package/dist/core/memory-engine/engine.js +149 -23
  56. package/dist/core/memory-engine/file-backend.d.ts +14 -2
  57. package/dist/core/memory-engine/file-backend.js +3 -0
  58. package/dist/core/memory-engine/index.d.ts +6 -3
  59. package/dist/core/memory-engine/index.js +4 -2
  60. package/dist/core/memory-engine/layout.js +20 -6
  61. package/dist/core/memory-engine/origin-clearance.d.ts +1 -1
  62. package/dist/core/memory-engine/scope-contract.d.ts +4 -4
  63. package/dist/core/memory-engine/sync-client.d.ts +16 -16
  64. package/dist/core/memory-engine/sync.d.ts +4 -4
  65. package/dist/core/memory-engine/types.d.ts +17 -0
  66. package/dist/core/memory-recall.d.ts +1 -1
  67. package/dist/core/memory.d.ts +2 -2
  68. package/dist/core/permission-rule-consent.d.ts +185 -36
  69. package/dist/core/permission-rule-consent.js +219 -44
  70. package/dist/core/permission-rule-model.d.ts +235 -31
  71. package/dist/core/permission-rule-model.js +105 -35
  72. package/dist/core/permission-rules.d.ts +9 -9
  73. package/dist/core/remote-env.d.ts +8 -8
  74. package/dist/core/roles.d.ts +3 -3
  75. package/dist/core/roles.js +1 -0
  76. package/dist/core/runner/assemble-result.d.ts +2 -2
  77. package/dist/core/runner/compaction-call-options.d.ts +3 -3
  78. package/dist/core/runner/memory-consolidation-driver.d.ts +49 -0
  79. package/dist/core/runner/memory-consolidation-driver.js +60 -0
  80. package/dist/core/runner/memory-consolidation.d.ts +1 -1
  81. package/dist/core/runner/prepare-config-doors.d.ts +3 -3
  82. package/dist/core/runner/prepare-task.d.ts +21 -21
  83. package/dist/core/runner/prepare-task.js +24 -17
  84. package/dist/core/runner/prepare-workspace-restore.d.ts +2 -2
  85. package/dist/core/runner/runtask.d.ts +11 -11
  86. package/dist/core/runner/runtask.js +4 -4
  87. package/dist/core/runner/session-rule-policy.d.ts +1 -1
  88. package/dist/core/runner/teardown-bounded.d.ts +1 -1
  89. package/dist/core/runner/tool-disclosure.d.ts +2 -2
  90. package/dist/core/runner/turn-attachments.d.ts +11 -11
  91. package/dist/core/scheduler.d.ts +5 -5
  92. package/dist/core/secret-env.d.ts +1 -1
  93. package/dist/core/sensitive-path-policy.d.ts +1 -1
  94. package/dist/core/session-policy-store.d.ts +2 -2
  95. package/dist/core/session-reconcile.d.ts +2 -2
  96. package/dist/core/session-store.d.ts +3 -3
  97. package/dist/core/session.d.ts +1 -1
  98. package/dist/core/shutdown-debug.d.ts +2 -2
  99. package/dist/core/side-query.d.ts +2 -2
  100. package/dist/core/spec-contract.d.ts +1 -1
  101. package/dist/core/store-contracts/contract-harness.d.ts +2 -2
  102. package/dist/core/store-contracts/contract-kit-version.d.ts +2 -2
  103. package/dist/core/store-contracts/mailbox-store-contract.d.ts +1 -1
  104. package/dist/core/store-contracts/mailbox-store-contract.js +1 -1
  105. package/dist/core/task-notification.d.ts +5 -5
  106. package/dist/core/task-registry-agent.d.ts +12 -12
  107. package/dist/core/task-registry-monitor.d.ts +1 -1
  108. package/dist/core/task-registry-shared.d.ts +41 -41
  109. package/dist/core/task-registry.d.ts +12 -12
  110. package/dist/core/tool-detach.d.ts +2 -2
  111. package/dist/core/tool-errors.d.ts +3 -3
  112. package/dist/core/tool-policy.d.ts +113 -28
  113. package/dist/core/tool-policy.js +80 -1
  114. package/dist/core/tool-result-budget.d.ts +1 -1
  115. package/dist/core/tool-result-store.d.ts +2 -2
  116. package/dist/core/tools.d.ts +1 -1
  117. package/dist/core/trace.d.ts +26 -23
  118. package/dist/core/types.d.ts +164 -86
  119. package/dist/core/types.js +4 -3
  120. package/dist/core/untrusted-egress.d.ts +1 -1
  121. package/dist/core/untrusted-text.d.ts +7 -7
  122. package/dist/core/wiring-manifest.d.ts +5 -5
  123. package/dist/core/workflow-journal-store.d.ts +14 -14
  124. package/dist/core/workflow-run-store-contract.d.ts +1 -1
  125. package/dist/core/workflow-run-store-contract.js +1 -1
  126. package/dist/core/workflow-run-store.d.ts +4 -4
  127. package/dist/engine/compaction/compaction.d.ts +3 -3
  128. package/dist/engine/compaction/utils.d.ts +2 -2
  129. package/dist/engine/execution-env/kill-tree.d.ts +1 -1
  130. package/dist/engine/execution-env/node-execution-env.d.ts +8 -8
  131. package/dist/engine/harness/agent-harness.d.ts +6 -6
  132. package/dist/engine/harness/messages.d.ts +1 -1
  133. package/dist/engine/harness/types.d.ts +10 -10
  134. package/dist/engine/llm/types.d.ts +14 -14
  135. package/dist/engine/loop/agent-loop.d.ts +3 -3
  136. package/dist/engine/loop/types.d.ts +4 -4
  137. package/dist/engine/lsp/node-lsp-manager.d.ts +2 -2
  138. package/dist/engine/session/import-validate.d.ts +1 -1
  139. package/dist/engine/session/log-digest.d.ts +1 -1
  140. package/dist/engine/session/memory-repo.d.ts +2 -2
  141. package/dist/engine/session/session.d.ts +4 -4
  142. package/dist/fixtures/index.d.ts +4 -4
  143. package/dist/index.d.ts +6 -5
  144. package/dist/index.js +5 -4
  145. package/dist/orchestration/goal.d.ts +1 -1
  146. package/dist/orchestration/run-spec.d.ts +1 -1
  147. package/dist/orchestration/run-workflow-tool.d.ts +12 -12
  148. package/dist/orchestration/workflow-governance.d.ts +4 -4
  149. package/dist/orchestration/workflow-observe.d.ts +1 -1
  150. package/dist/orchestration/workflow-script-runner.d.ts +1 -1
  151. package/dist/orchestration/workflow-script-store.d.ts +9 -9
  152. package/dist/orchestration/workflow-size-guideline.d.ts +1 -1
  153. package/dist/orchestration/workflow-types.d.ts +5 -5
  154. package/dist/orchestration/workflow.d.ts +10 -10
  155. package/dist/prompt-assembly/artifact-store.d.ts +1 -1
  156. package/dist/prompt-assembly/artifact.d.ts +1 -1
  157. package/dist/prompt-assembly/assemble.d.ts +1 -1
  158. package/dist/prompt-assembly/composer.d.ts +2 -2
  159. package/dist/prompt-assembly/epoch.d.ts +2 -2
  160. package/dist/prompt-assembly/event-registry.d.ts +1 -1
  161. package/dist/prompt-assembly/explain.d.ts +3 -3
  162. package/dist/prompt-assembly/tool-catalog.d.ts +1 -1
  163. package/dist/prompt-assembly/turn-snapshot.d.ts +4 -4
  164. package/dist/prompt-assembly/types.d.ts +12 -12
  165. package/dist/prompts/coordinator.d.ts +1 -1
  166. package/dist/prompts/default.d.ts +8 -8
  167. package/dist/prompts/simple-sections.d.ts +3 -3
  168. package/dist/prompts/supervisor.d.ts +2 -2
  169. package/dist/scenarios/full-body.d.ts +3 -3
  170. package/dist/scenarios/scenario-registry.d.ts +1 -1
  171. package/dist/stores/cc/sidecar-transcript.d.ts +3 -3
  172. package/dist/stores/file/fs-atomic.d.ts +2 -2
  173. package/dist/stores/file/index.d.ts +1 -1
  174. package/dist/stores/file/session-store.d.ts +2 -2
  175. package/dist/stores/file/workflow-journal-store.d.ts +4 -4
  176. package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -1
  177. package/dist/tools/fs/encoding.d.ts +4 -4
  178. package/dist/tools/fs/fs-bash.d.ts +3 -3
  179. package/dist/tools/fs/fs-pdf.d.ts +1 -1
  180. package/dist/tools/fs/fs-shared.d.ts +6 -6
  181. package/dist/tools/fs/index.d.ts +2 -2
  182. package/dist/tools/fs/notebook.d.ts +1 -1
  183. package/dist/tools/fs/pdf.d.ts +1 -1
  184. package/dist/tools/fs/read-deny.d.ts +1 -1
  185. package/dist/tools/fs/safety.d.ts +9 -9
  186. package/dist/tools/fs/search.d.ts +2 -2
  187. package/dist/tools/monitor.d.ts +3 -3
  188. package/dist/tools/task-list.d.ts +2 -2
  189. package/dist/tools/web.d.ts +4 -4
  190. package/dist/tools/worktree.d.ts +5 -5
  191. package/package.json +1 -1
  192. package/test/export-surface.snapshot.json +1826 -3
@@ -32,6 +32,12 @@
32
32
  * deny/ask layer (`permission-rule-org.ts`) judges every segment and runs ahead of the allow lane at
33
33
  * the gate — a compound-prefix allow can therefore never outrun a deny on any segment it admits.
34
34
  *
35
+ * design/375 adds upstream's OTHER half of that per-segment evaluation on the allow side: a compound
36
+ * whose EVERY segment is admitted by some eligible rule is covered by the CONJUNCTION of those rules
37
+ * (`findAdmittingRule` answers with the coverage SET). No single rule widens: the matcher predicates
38
+ * above are untouched, and the set form is the decision layer's answer, fenced by the same
39
+ * ahead-running per-segment deny/ask layer.
40
+ *
35
41
  * The floor is `parseLeadingCommandName` + `splitShellCompoundSegments` — the one simple-command parser
36
42
  * and the one segmentation, both already shared with the read-only classifier, the reversibility probe,
37
43
  * the coarse command-name policy and the skill tool specifier. A second tokenizer would drift, and drift
@@ -276,6 +282,40 @@ export declare const SUGGESTION_LEXICON: readonly string[];
276
282
  * through, so an ordinary rule text reads normally. The result is length-bounded.
277
283
  */
278
284
  export declare function escapeForDisclosure(value: unknown): string;
285
+ /**
286
+ * Render raw, untrusted COMMAND text for a display surface — the minimal safe baseline for the seats
287
+ * that carry post-rewrite command bytes verbatim ({@link SegmentRuleSuggestion.segment} is the
288
+ * motivating one; a card's own `command`/`args` echo has the same hazard).
289
+ *
290
+ * WHAT IT DOES, and why the two character families are not handled the same way:
291
+ * · `\p{Cf}` (the bidirectional controls U+061C/U+200E/U+200F/U+202A–U+202E/U+2066–U+2069 and the
292
+ * zero-width formatters ZWSP/ZWNJ/ZWJ/WORD JOINER/SOFT HYPHEN/BOM) is REMOVED. These occupy no
293
+ * width, so deleting them is what makes the rendered line agree with what a reader believes they
294
+ * are reading — a `rm -rf x` that DISPLAYS as something benign is exactly the deception the seat
295
+ * would otherwise hand on. This is the same family
296
+ * {@link import("./tool-policy.js").AskRequest.hasBidiControls} raises its warning bit for; the
297
+ * bit says "the raw bytes carry them", this function is what a surface renders instead.
298
+ * · Controls and line/paragraph separators (C0 incl. TAB and NEWLINE, DEL, C1, U+2028/U+2029) are
299
+ * FOLDED TO A SPACE with runs collapsed, not removed: they occupy width (or a whole line), and
300
+ * deleting them would glue two tokens into one word that reads as a different command. TAB is not
301
+ * excepted here — a one-line display value has no use for one, and a rule text's foldable-tab
302
+ * exception belongs to the VALIDATOR class, not to this one.
303
+ * The result is then capped by code point (never splitting a surrogate pair) with a `…` marker, and
304
+ * carries the shared authority-envelope neutralization every untrusted inline value gets — this text
305
+ * can land on a model-visible line as easily as on a human one.
306
+ *
307
+ * WHAT IT IS NOT. A DISPLAY baseline, never an identity channel and never adjudication input: the
308
+ * transform is LOSSY and NOT injective (two distinct commands can render identically — that is the
309
+ * price of a readable line). Correlate, match, store and adjudicate on the RAW bytes, which the
310
+ * carrying seats keep unchanged: this function neither changes what those seats contain nor what any
311
+ * of them means. A caller that must instead tell two hostile texts APART wants
312
+ * {@link escapeForDisclosure} (which escapes rather than folds, so nothing collides) or
313
+ * {@link hasUnrenderableCharacters} (refuse instead of render).
314
+ *
315
+ * @param text the raw untrusted text (a `String(x)` is applied to anything that is not one).
316
+ * @param maxLen code-point cap for the rendered result; defaults to the disclosure-line budget.
317
+ */
318
+ export declare function renderUntrustedCommandText(text: unknown, maxLen?: number): string;
279
319
  /**
280
320
  * Does this text carry a character of the class {@link escapeForDisclosure} exists for — the class
281
321
  * {@link CONTROL_CHARS_RE} names? Asked by a caller that must REFUSE such a value rather than render
@@ -367,35 +407,188 @@ export declare function scopeCoversCwd(scope: RuleScope, cwd: string | undefined
367
407
  * when tombstones are applied, so a rule with an empty `adds` is a rule that no longer exists. */
368
408
  export declare function isRuleLive(rule: PersistedAllowRule): boolean;
369
409
  /**
370
- * Find the rule that admits this command for a task in `cwd`, or `undefined`.
410
+ * design/375 §5.1 — MAY this rule speak about this call's CONTEXT? Tool identity plus scope coverage,
411
+ * and nothing about the rule's liveness: the CONTEXT question is the half a not-yet-persisted rule (a
412
+ * card's proposed candidate, which has no add dots yet) can already answer.
413
+ *
414
+ * The tool conjunct is not decoration: more than one tool takes a shell command string, and matching on
415
+ * the command alone would let an approval given on one surface resolve a call on another. The scope
416
+ * conjunct is the project fence — project A's approval is not project B's, and a project rule with no
417
+ * cwd to compare against covers nothing (fail-closed, {@link scopeCoversCwd}).
371
418
  *
372
- * Order is defined only for reporting (the first match wins and its text is what the decision names);
373
- * no rule is stronger than another they are all the same one answer, "the person already said yes to
374
- * this".
419
+ * `scope` rides beside the rule rather than inside it for the PROPOSED half's sake: a
420
+ * {@link ParsedAllowRule} carries no scope, so a proposal is judged against the scope it would LAND in.
421
+ */
422
+ export declare function eligibleContext(rule: {
423
+ tool: string;
424
+ scope: RuleScope;
425
+ }, call: {
426
+ tool: string;
427
+ cwd: string | undefined;
428
+ }): boolean;
429
+ /**
430
+ * design/375 §5.1 — MAY this PERSISTED rule participate in admitting this call at all? The liveness
431
+ * layer on top of {@link eligibleContext}: a tombstoned rule (zero surviving adds) is a rule that no
432
+ * longer exists and must not count as coverage anywhere.
433
+ *
434
+ * THE single eligibility predicate for every consumer that reads the persisted store — the gate's
435
+ * conjunction arm and its coverage-set selection here, the suggestion face's coverage judgement and the
436
+ * consent prepare's ({@link segmentCoverageOf} is the shared instrument). One predicate, so a rule that
437
+ * one consumer refuses cannot be a rule another counts: the historically inlined trio of checks
438
+ * (live / tool / scope) is this function, verbatim.
439
+ */
440
+ export declare function eligiblePersisted(rule: PersistedAllowRule, call: {
441
+ tool: string;
442
+ cwd: string | undefined;
443
+ }): boolean;
444
+ /**
445
+ * Find the rules that admit this command for a task in `cwd`, or `undefined`.
446
+ *
447
+ * THE ANSWER IS A COVERAGE SET (design/375 §5.1, the upstream per-segment form): a single rule that
448
+ * admits the whole command is a one-element set — the historical arm, table order preserved verbatim —
449
+ * and a COMPOUND whose every segment is admitted by some eligible rule is covered by the CONJUNCTION of
450
+ * those rules, returned as the set that covered it. The judging ladder, in order:
451
+ * 1. the fail-closed floor (a shape the lane cannot read matches nothing), evaluated once;
452
+ * 2. the single-rule arm — whole-string exact, single/compound-prefix — exactly the historical loop:
453
+ * first table-order match wins and is what the decision names. A single rule naming the command is
454
+ * found BEFORE any conjunction: reporting one rule is narrower than reporting a set;
455
+ * 3. the conjunction arm, compounds only: every segment must be admitted by some eligible rule under
456
+ * the MATCH reading (the allow side's own under-approximating instrument — never the deny side's
457
+ * wider PROGRAM_RUNS reading). Per segment the FIRST table-order admitting rule is selected, and
458
+ * the coverage set is those selections in segment order, de-duplicated — the same command against
459
+ * the same rule table always names the same set (reporting-order determinism, the historical
460
+ * "order is defined only for reporting" contract extended to sets).
461
+ *
462
+ * What the conjunction arm deliberately does NOT change: {@link ruleAdmitsCommand} itself. A
463
+ * single-command prefix body still never admits a chain — `Bash(npm:*)` does not admit
464
+ * `npm test && curl evil.example` — but the chain is now admitted when EVERY segment has a standing
465
+ * yes, which is the upstream decision form (per-segment allow, any-segment deny decided strictly
466
+ * earlier at the gate's org layer). A trailing-terminator spelling (`a && b ;`) keeps the conservative
467
+ * answer by construction: under the MATCH reading its tail is an empty segment the floor refuses, so
468
+ * the whole command is outside the lane and asks.
469
+ *
470
+ * Order is defined only for reporting (what the decision names); no rule is stronger than another —
471
+ * they are all the same one answer, "the person already said yes to this".
375
472
  */
376
473
  export declare function findAdmittingRule(rules: readonly PersistedAllowRule[], call: {
377
474
  tool: string;
378
475
  command: string;
379
476
  cwd: string | undefined;
380
- }): PersistedAllowRule | undefined;
381
- /** A rule the engine offers to mint, as carried on an approval card. */
382
- export interface RuleSuggestion {
477
+ }): readonly PersistedAllowRule[] | undefined;
478
+ /**
479
+ * design/375 §5.2 — the per-segment coverage table for `command`: which segments an eligible rule
480
+ * already admits, judged with the SAME split, the same MATCH reading and the same eligibility
481
+ * predicates as the gate's conjunction arm — one instrument, so the suggestion face and the consent
482
+ * prepare can never call a segment covered that the gate would ask about (or vice versa).
483
+ *
484
+ * TWO SOURCES, two predicates, and the seat labels are the contract:
485
+ * · `persisted` — rules already in the store, judged {@link eligiblePersisted} ∧ admit: a tombstoned
486
+ * rule, another project's rule and another tool's rule are uniformly NOT coverage;
487
+ * · `proposed` — rules of the CURRENT batch that are not yet persisted (no add dots yet), judged
488
+ * {@link eligibleContext} ∧ admit against the scope each would land in. Judging these with the
489
+ * persisted predicate would make "this batch, once redeemed, covers the command" permanently false
490
+ * — the liveness layer is the one thing a proposal legitimately lacks.
491
+ *
492
+ * Returns `undefined` for a command the lane cannot read at all (the fail-closed floor — the caller's
493
+ * honest answer is then "no coverage table", which every consumer must treat as all-uncovered). The
494
+ * reported `segment` texts are the FOLDED spellings, trimmed — display/correlation seats, never
495
+ * adjudication inputs.
496
+ */
497
+ export declare function segmentCoverageOf(command: string, rules: {
498
+ persisted: readonly PersistedAllowRule[];
499
+ proposed?: readonly {
500
+ rule: ParsedAllowRule;
501
+ scope: RuleScope;
502
+ }[];
503
+ }, call: {
504
+ tool: string;
505
+ cwd: string | undefined;
506
+ }): readonly SegmentCoverage[] | undefined;
507
+ /** One row of a per-segment coverage table (see {@link segmentCoverageOf}): the FOLDED, trimmed
508
+ * segment text (a display/correlation seat, never adjudication input) and whether an eligible rule
509
+ * already admits it. */
510
+ export interface SegmentCoverage {
511
+ readonly segment: string;
512
+ readonly covered: boolean;
513
+ }
514
+ /** One member of a {@link RuleOffer} batch: a rule the engine offers to mint for ONE segment of a
515
+ * compound command. `segment` is the FOLDED segment text the member was minted from — a rendering
516
+ * seat, never adjudication input. */
517
+ export interface SegmentRuleSuggestion {
383
518
  /** The canonical rule text the host would redeem. */
384
- rule: string;
385
- match: PersistedRuleMatch;
519
+ readonly rule: string;
520
+ readonly match: PersistedRuleMatch;
386
521
  /** The command pattern in the rule, for a host that wants to render it without re-parsing. */
387
- command: string;
522
+ readonly command: string;
523
+ /** The (folded) segment this suggestion came from. Raw post-rewrite command bytes — a substring
524
+ * of the same call's `args`/`command` values riding beside it on every surface that carries
525
+ * this seat — so render it with contextual escaping, exactly like those.
526
+ *
527
+ * RAW is the contract here and does not change: this seat carries the bytes, because the bytes are
528
+ * what a rule would be minted from and what a correlation keys on. The rendering side of that
529
+ * contract is {@link renderUntrustedCommandText} — the shipped display baseline (bidi and
530
+ * zero-width controls removed, controls/line separators folded, length capped, envelope-neutralized)
531
+ * a surface can adopt instead of writing its own, since raw command bytes displayed unprocessed can
532
+ * show a reader something other than what they authorize. */
533
+ readonly segment: string;
388
534
  }
389
535
  /**
390
- * The 1-2 candidates offered on an approval card for `command`.
536
+ * design/375 §3.1 ONE "don't ask again" option on an approval card. A CLOSED discriminated union
537
+ * (`kind`); consumers parse it per element, and an element whose `kind` they do not know is dropped
538
+ * (single-row degrade) while keeping the ORIGINAL wire index for every element they keep — selection
539
+ * and redemption are keyed on original indices, so a consumer that cannot preserve the mapping must
540
+ * suppress its persistence actions entirely (fail toward asking).
541
+ *
542
+ * · `kind: "single"` — one rule covering the WHOLE command (the exact spelling, or a broader
543
+ * reviewed prefix form for a simple command).
544
+ * · `kind: "batch"` — a CONJUNCTION batch of per-segment rules for a compound command. Choosing it
545
+ * is one yes to ALL of `rules` at once — there is no per-member selection inside a batch; a
546
+ * consumer that wants a narrower answer picks the single offer or approves once without a rule.
547
+ * `rules` has 1..5 members (the card-lane minting cap), in segment order, deduplicated by rule
548
+ * text. `uncoveredSegments` is the honest surplus disclosure of the cap: judged at MINT time
549
+ * against the coverage snapshot the card was drawn from ∪ this batch's FINAL (deduplicated,
550
+ * capped) rules, it counts the violating segments still admitted by neither — 0 means "once this
551
+ * batch is redeemed, this compound is fully covered by that snapshot's lights". It is a statement
552
+ * about the minting snapshot, not a standing guarantee: a concurrent rule removal can date it.
553
+ *
554
+ * Array contract (`ruleOffers`, the durable park row, and the consent record's projection while
555
+ * the card is PENDING all share it): at most 2 offers; the whole-string exact single is index 0
556
+ * whenever present; the batch offer — at most one — is always last. It is a draw-time contract,
557
+ * not a settled-record invariant: confirming an EDITED candidate appends a third single offer
558
+ * AFTER the batch in the same write that settles the record, so a SETTLED record's projection may
559
+ * carry it. ADVISORY display metadata, never adjudication input, and never a
560
+ * rule by itself: minting is a separate act that goes through the approval-record protocol, so a
561
+ * surface that ignores offers entirely changes nothing about what is allowed. The authoritative
562
+ * render source for a card is the PREPARED record's own offers (record-is-what-you-see); the ask-time
563
+ * projection here may drift from it across a coverage change, and the drift direction is harmless
564
+ * (the record is what gets confirmed).
565
+ */
566
+ export type RuleOffer = {
567
+ readonly kind: "single";
568
+ /** The canonical rule text the host would redeem. */
569
+ readonly rule: string;
570
+ readonly match: PersistedRuleMatch;
571
+ /** The command pattern in the rule, for a host that wants to render it without re-parsing. */
572
+ readonly command: string;
573
+ } | {
574
+ readonly kind: "batch";
575
+ /** 1..5 per-segment rules, segment order, deduplicated by rule text. */
576
+ readonly rules: readonly SegmentRuleSuggestion[];
577
+ /** Mint-time honest surplus: violating segments admitted by neither the coverage snapshot nor
578
+ * this batch's final rules (see the union doc above for the exact arithmetic). */
579
+ readonly uncoveredSegments: number;
580
+ };
581
+ /**
582
+ * The offers presented on an approval card for `command` (design/375 §5.2).
391
583
  *
392
- * **Array order is a documented CONTRACT, not an implementation accident**: display order = array
393
- * order = narrowest first. The EXACT form (this whole command line) is always index 0 whenever
394
- * anything is offered at all; a broader reviewed PREFIX form — at most one follows at index 1.
395
- * Selection indices and redemption tickets are index-keyed against this order (a card's
396
- * `selectedCandidate` and its `rt.<index>.` tickets), so consumers may rely on it.
584
+ * **Array order is a documented CONTRACT, not an implementation accident** (see {@link RuleOffer}):
585
+ * the whole-string EXACT single is index 0 whenever present; a SIMPLE command may carry a broader
586
+ * reviewed PREFIX single after it; a COMPOUND carries at most one BATCH offer, always last.
587
+ * Selection indices are keyed against this order, so consumers may rely on it; redemption TICKETS
588
+ * are NOT they key on the consent record's flat CANDIDATE list, and a chosen offer is redeemed
589
+ * via `redeemRuleBatch` (see the consent record's own doc for the two index spaces).
397
590
  *
398
- * The prefix candidate is the LONGEST body two sources agree to offer: {@link SUGGESTION_LEXICON}'s
591
+ * The prefix single is the LONGEST body two sources agree to offer: {@link SUGGESTION_LEXICON}'s
399
592
  * deeper-than-two-words rows first, and otherwise {@link genericPrefixBody}'s general two-word form
400
593
  * (`argv[0]` + a token shaped like a subcommand). Read those two for the grammar, its adopted origin,
401
594
  * and the operand false positive it accepts; the short version is that a bare verb (`git`), an
@@ -407,9 +600,10 @@ export interface RuleSuggestion {
407
600
  * "no prefix".
408
601
  *
409
602
  * Every produced candidate must survive the round trip — parse as a rule, come back as the match form
410
- * this seat is offering, AND admit the very command it was minted from. Enforced on BOTH seats,
411
- * fail-closed: a candidate that would not round-trip is silently not offered, since offering an option
412
- * redemption would refuse is worse than offering one fewer.
603
+ * its seat is offering, AND admit the very text it was minted from (the whole command for a single,
604
+ * its own segment for a batch member). Enforced on EVERY seat, fail-closed: a candidate that would
605
+ * not round-trip is silently not offered, since offering an option redemption would refuse is worse
606
+ * than offering one fewer.
413
607
  *
414
608
  * The FORM half of that check is not decoration. A command may end in the rule grammar's own prefix
415
609
  * marker — `rm :*` is a legal thing to type — and wrapping it as an exact rule produces the text
@@ -419,19 +613,29 @@ export interface RuleSuggestion {
419
613
  * for a lexicon command like `git status :*`, emitted twice — once mislabelled, once as the real prefix
420
614
  * candidate). The seat therefore believes the PARSER about what it got back, never its own request.
421
615
  *
422
- * A COMPOUND (`./gradlew build && ./gradlew test`) fills the exact seat — the offered rule spells the
423
- * whole chain and admits exactly it (the shape this seat was missing: the ordinary build invocation is
424
- * a connector chain, and a card that could offer nothing for it made every such command a fresh
425
- * question forever) and, when its FINAL segment yields a prefix body under the very same grammar the
426
- * single-command seat uses, a COMPOUND-PREFIX candidate besides: the leading segments verbatim,
427
- * connectors and all, with the final segment cut down to its body (`cd /tmp && adb pull /sdcard/x .`
428
- * offers `Bash(cd /tmp && adb pull:*)`). A final segment the grammar declines a flag in second
429
- * position, a screened head, one tokenleaves the chain with its exact seat alone, exactly as the
430
- * same shape leaves a simple command. No seat is ever minted off a NON-final segment: those are the
431
- * byte-exact half of the rule, so there is nothing to widen.
616
+ * A COMPOUND (`./gradlew build && ./gradlew test`) fills the exact single — the offered rule spells
617
+ * the whole chain and admits exactly it and a BATCH offer besides: every segment NOT already
618
+ * covered (per `ctx.coverage`) yields its own single-segment best form a prefix when the
619
+ * single-command grammar grants one for that segment's tokens (the interception screens apply
620
+ * unchanged), otherwise the segment's exact deduplicated by rule text in segment order and capped
621
+ * at FIVE (upstream's own batch cap; the overflow is disclosed via `uncoveredSegments` rather than
622
+ * silently truncated away). A segment whose every form the floor refuses simply contributes no
623
+ * member (and stays counted as uncovered — the honest gap). The COMPOUND-PREFIX candidate this seat
624
+ * used to mint here is deliberately NO LONGER OFFERED (design/375 §5.3): the batch takes its card
625
+ * position, while the compound-prefix GRAMMAR remains a fully supported rule family the matcher,
626
+ * the hand-written/edited/import channels and every persisted such rule are untouched.
627
+ *
628
+ * `ctx.coverage` is the per-segment coverage table minted by the SAME store read that adjudicated
629
+ * the call (see {@link segmentCoverageOf} and the gate's engine-stamped seat) — positional over this
630
+ * function's own folded segments, belt-checked by text: a table whose length or segment texts do not
631
+ * line up is discarded whole, and an absent or discarded table reads as ALL-UNCOVERED (fail toward
632
+ * over-offering; every offered rule still passes the validator at redemption, so an over-offer widens
633
+ * nothing).
432
634
  *
433
635
  * Returns an empty array for anything the rule lane cannot speak for (redirections, substitutions,
434
636
  * subshells, backgrounding) — the card then simply carries no "don't ask again" option, which is the
435
637
  * honest answer.
436
638
  */
437
- export declare function suggestRulesForCommand(command: string): RuleSuggestion[];
639
+ export declare function suggestRulesForCommand(command: string, ctx?: {
640
+ readonly coverage?: readonly SegmentCoverage[];
641
+ }): RuleOffer[];
@@ -1,5 +1,6 @@
1
1
  import { parsePermissionRule } from "./permission-rules.js";
2
2
  import { parseLeadingCommandName, splitShellCompoundSegments } from "../tools/fs/bash-readonly-classifier.js";
3
+ import { inlineUntrusted } from "./untrusted-text.js";
3
4
  export const MAX_RULE_TEXT_CHARS = 512;
4
5
  export const BARE_INTERPRETER_NAMES = new Set([
5
6
  "node", "deno", "bun", "python", "python2", "python3", "perl", "ruby", "php", "osascript",
@@ -162,6 +163,17 @@ export function escapeForDisclosure(value) {
162
163
  });
163
164
  return escaped.length <= DISCLOSED_RULE_TEXT_MAX_CHARS ? escaped : `${escaped.slice(0, DISCLOSED_RULE_TEXT_MAX_CHARS)}…`;
164
165
  }
166
+ const DISPLAY_STRIP_FORMAT_RE = /\p{Cf}/gu;
167
+ export function renderUntrustedCommandText(text, maxLen = DISCLOSED_RULE_TEXT_MAX_CHARS) {
168
+ let raw;
169
+ try {
170
+ raw = typeof text === "string" ? text : String(text);
171
+ }
172
+ catch {
173
+ return "<unprintable>";
174
+ }
175
+ return inlineUntrusted(raw.replace(DISPLAY_STRIP_FORMAT_RE, ""), maxLen);
176
+ }
165
177
  export function hasUnrenderableCharacters(text) {
166
178
  return CONTROL_CHARS_RE.test(text);
167
179
  }
@@ -247,60 +259,118 @@ export function scopeCoversCwd(scope, cwd) {
247
259
  export function isRuleLive(rule) {
248
260
  return rule.adds.length > 0;
249
261
  }
250
- export function findAdmittingRule(rules, call) {
251
- const shape = ruleLaneShapeOf(call.command, MATCH_READING);
252
- if ("reject" in shape)
253
- return undefined;
262
+ export function eligibleContext(rule, call) {
263
+ return rule.tool === call.tool && scopeCoversCwd(rule.scope, call.cwd);
264
+ }
265
+ export function eligiblePersisted(rule, call) {
266
+ return isRuleLive(rule) && eligibleContext(rule, call);
267
+ }
268
+ function firstEligibleAdmittingRule(rules, commandText, call) {
254
269
  for (const rule of rules) {
255
- if (!isRuleLive(rule))
256
- continue;
257
- if (rule.tool !== call.tool)
270
+ if (!eligiblePersisted(rule, call))
258
271
  continue;
259
- if (!scopeCoversCwd(rule.scope, call.cwd))
260
- continue;
261
- if (ruleAdmitsCommand(rule, call.command))
272
+ if (ruleAdmitsCommand(rule, commandText))
262
273
  return rule;
263
274
  }
264
275
  return undefined;
265
276
  }
266
- export function suggestRulesForCommand(command) {
277
+ export function findAdmittingRule(rules, call) {
278
+ const shape = ruleLaneShapeOf(call.command, MATCH_READING);
279
+ if ("reject" in shape)
280
+ return undefined;
281
+ const whole = firstEligibleAdmittingRule(rules, call.command, call);
282
+ if (whole !== undefined)
283
+ return [whole];
284
+ if (shape.segments.length < 2)
285
+ return undefined;
286
+ const covering = [];
287
+ for (const segment of shape.segments) {
288
+ const first = firstEligibleAdmittingRule(rules, segment, call);
289
+ if (first === undefined)
290
+ return undefined;
291
+ if (!covering.includes(first))
292
+ covering.push(first);
293
+ }
294
+ return covering;
295
+ }
296
+ export function segmentCoverageOf(command, rules, call) {
297
+ const folded = foldSpacing(command);
298
+ if (folded === undefined)
299
+ return undefined;
300
+ const shape = ruleLaneShapeOf(folded, MATCH_READING);
301
+ if ("reject" in shape)
302
+ return undefined;
303
+ const proposed = rules.proposed ?? [];
304
+ return shape.segments.map((segment) => ({
305
+ segment: segment.trim(),
306
+ covered: firstEligibleAdmittingRule(rules.persisted, segment, call) !== undefined ||
307
+ proposed.some((p) => eligibleContext({ tool: p.rule.tool, scope: p.scope }, call) && ruleAdmitsCommand(p.rule, segment)),
308
+ }));
309
+ }
310
+ export function suggestRulesForCommand(command, ctx) {
267
311
  const shape = ruleLaneShapeOf(command, MATCH_READING);
268
312
  if ("reject" in shape)
269
313
  return [];
270
314
  const folded = foldSpacing(command);
271
315
  if (folded === undefined)
272
316
  return [];
273
- const out = [];
317
+ const offers = [];
274
318
  const exact = parseAllowRuleText(formatAllowRuleText(folded, "exact"));
275
319
  if ("rule" in exact && exact.rule.match === "exact" && ruleAdmitsCommand(exact.rule, command)) {
276
- out.push({ rule: exact.rule.rule, match: "exact", command: exact.rule.command });
320
+ offers.push({ kind: "single", rule: exact.rule.rule, match: "exact", command: exact.rule.command });
277
321
  }
278
- const offerPrefix = (body) => {
279
- if (body === undefined)
280
- return;
281
- const text = formatAllowRuleText(body, "prefix");
282
- const parsed = parseAllowRuleText(text);
283
- if ("rule" in parsed && parsed.rule.match === "prefix" && ruleAdmitsCommand(parsed.rule, command)) {
284
- out.push({ rule: parsed.rule.rule, match: "prefix", command: parsed.rule.command });
285
- }
286
- };
287
322
  const tokensOf = (s) => s.split(/\s+/).filter((t) => t !== "");
288
323
  const bodyOf = (tokens) => longestReviewedBody(tokens) ?? genericPrefixBody(tokens);
289
- if (out.length === 1) {
290
- if (shape.segments.length === 1) {
291
- offerPrefix(bodyOf(tokensOf(folded)));
292
- }
293
- else {
294
- const foldedShape = ruleLaneShapeOf(folded, MATCH_READING);
295
- if (!("reject" in foldedShape) && foldedShape.segments.length === shape.segments.length) {
296
- const rawLast = foldedShape.segments[foldedShape.segments.length - 1] ?? "";
297
- const lastBody = bodyOf(tokensOf(rawLast));
298
- if (lastBody !== undefined) {
299
- const cut = folded.length - rawLast.length + (rawLast.length - rawLast.trimStart().length);
300
- offerPrefix(folded.slice(0, cut) + lastBody);
324
+ if (shape.segments.length === 1) {
325
+ if (offers.length === 1) {
326
+ const body = bodyOf(tokensOf(folded));
327
+ if (body !== undefined) {
328
+ const parsed = parseAllowRuleText(formatAllowRuleText(body, "prefix"));
329
+ if ("rule" in parsed && parsed.rule.match === "prefix" && ruleAdmitsCommand(parsed.rule, command)) {
330
+ offers.push({ kind: "single", rule: parsed.rule.rule, match: "prefix", command: parsed.rule.command });
301
331
  }
302
332
  }
303
333
  }
334
+ return offers;
335
+ }
336
+ const foldedShape = ruleLaneShapeOf(folded, MATCH_READING);
337
+ if ("reject" in foldedShape || foldedShape.segments.length !== shape.segments.length)
338
+ return offers;
339
+ const segments = foldedShape.segments.map((s) => s.trim());
340
+ const table = ctx?.coverage;
341
+ const alignedTable = table !== undefined && table.length === segments.length && table.every((row, i) => row.segment === segments[i]) ? table : undefined;
342
+ const coveredAt = (i) => alignedTable?.[i]?.covered === true;
343
+ const mintSegmentRule = (segment) => {
344
+ const body = bodyOf(tokensOf(segment));
345
+ if (body !== undefined) {
346
+ const parsed = parseAllowRuleText(formatAllowRuleText(body, "prefix"));
347
+ if ("rule" in parsed && parsed.rule.match === "prefix" && ruleAdmitsCommand(parsed.rule, segment)) {
348
+ return { rule: parsed.rule.rule, match: "prefix", command: parsed.rule.command };
349
+ }
350
+ }
351
+ const segExact = parseAllowRuleText(formatAllowRuleText(segment, "exact"));
352
+ if ("rule" in segExact && segExact.rule.match === "exact" && ruleAdmitsCommand(segExact.rule, segment)) {
353
+ return { rule: segExact.rule.rule, match: "exact", command: segExact.rule.command };
354
+ }
355
+ return undefined;
356
+ };
357
+ const minted = [];
358
+ for (let i = 0; i < segments.length; i++) {
359
+ if (coveredAt(i))
360
+ continue;
361
+ const segment = segments[i];
362
+ const rule = mintSegmentRule(segment);
363
+ if (rule !== undefined && !minted.some((m) => m.rule === rule.rule))
364
+ minted.push({ ...rule, segment });
365
+ }
366
+ const batchRules = minted.slice(0, 5);
367
+ if (batchRules.length > 0) {
368
+ const parsedBatch = batchRules.flatMap((r) => {
369
+ const p = parseAllowRuleText(r.rule);
370
+ return "rule" in p ? [p.rule] : [];
371
+ });
372
+ const uncoveredSegments = segments.filter((segment, i) => !coveredAt(i) && !parsedBatch.some((p) => ruleAdmitsCommand(p, segment))).length;
373
+ offers.push({ kind: "batch", rules: batchRules, uncoveredSegments });
304
374
  }
305
- return out;
375
+ return offers;
306
376
  }
@@ -7,7 +7,7 @@
7
7
  * run-loop 介入的「声明式数据→ToolPolicy」工厂(`createCoarseCommandNamePolicy` 同形态先例)。
8
8
  * core 不读设置文件、不管 source 层级合并 —— Settings-loader 把各层合并成**单一规则表**后编译
9
9
  * **一个** policy(不要按层各编译再组合:combinePolicies 的 fold 里 ask 胜 allow,跨 policy 的
10
- * 显式 allow 规则会被另一 policy 的 ask 吞掉 —— codex 127 审 B3)。
10
+ * 显式 allow 规则会被另一 policy 的 ask 吞掉)。
11
11
  *
12
12
  * 🔴 与 CC 的三处有意差(design/127 r2):
13
13
  * - CC 参数级 generic 道无 allow lane(gnn 只有 deny/ask)—— 我们同样:`allow` + 参数级 content =
@@ -16,7 +16,7 @@
16
16
  * **工厂 validator strict**:降级即 invalid(fail-fast 世界观,写错的规则不能静默变宽)。
17
17
  * - 已知主字段参数规则(如 `Bash(command:…)`)默认 **reject**:通配是 trim 后**整串 lexical**
18
18
  * 匹配,不是 shell AST / 前缀 lane —— `Bash(command:git push*)` 匹配不了 `cd /x && git push`,
19
- * 「以为挡住了」比没挡更危险(codex 127 审 M2)。`primaryFieldGeneric:"allow-lexical"` 显式解锁。
19
+ * 「以为挡住了」比没挡更危险。`primaryFieldGeneric:"allow-lexical"` 显式解锁。
20
20
  */
21
21
  import type { NamedToolPolicy } from "./tool-policy.js";
22
22
  /** [K-PLATFORM-SWEEP] MINOR#11: Bash's REAL non-primary arg names, accepted as generic param rules
@@ -28,7 +28,7 @@ export interface PermissionRule {
28
28
  /** CC 语法:`"Tool"` 或 `"Tool(param:pattern)"`。`Tool()`/`Tool(*)` 归一为裸规则;括号可 `\(`/`\)` 转义。 */
29
29
  rule: string;
30
30
  behavior: "deny" | "ask" | "allow";
31
- /** Provenance(codex 127 审 M7):进 deny/ask 的 message 供审计(e.g. `"policySettings"`)。层级
31
+ /** Provenance:进 deny/ask 的 message 供审计(e.g. `"policySettings"`)。层级
32
32
  * 优先级/tighten-only 语义在 Settings-loader,不在这里。 */
33
33
  source?: string;
34
34
  }
@@ -40,7 +40,7 @@ export interface ParsedPermissionRule {
40
40
  }
41
41
  export interface PermissionRuleIssue {
42
42
  rule: string;
43
- /** `invalid.*` = 语法/语义错;`unsupported.*` = CC 合法但 v1 不支持的 lane(codex 127 审 B5:独立
43
+ /** `invalid.*` = 语法/语义错;`unsupported.*` = CC 合法但 v1 不支持的 lane(独立
44
44
  * 分类,绝不静默 no-op —— `Bash(npm run:*)` 被当 generic 参数规则接受后永不匹配是最危险的失败形态)。 */
45
45
  code: "invalid.empty_tool" | "invalid.paren" | "invalid.param_split" | "invalid.allow_param_rule" | "invalid.primary_field" | "invalid.cap_exceeded" | "unsupported.bash_prefix"
46
46
  /** design/179 D7: a Bash prefix rule under `bashPrefixLane:"rules-store"` — reported so a migration
@@ -65,20 +65,20 @@ export interface PermissionRuleCaps {
65
65
  maxStars: number;
66
66
  /** Scalar arg values longer than this are NOT matched textually — the rule **fail-closes** to its
67
67
  * lane (a deny rule targeting an oversized value denies; an ask rule asks). 租户规则 × 模型巨串
68
- * 的热路径成本上界(codex 127 审 M6)。 */
68
+ * 的热路径成本上界。 */
69
69
  maxScalarValueChars: number;
70
70
  }
71
71
  export interface PermissionRulePolicyOptions {
72
72
  /** Invalid/unsupported rules: `"throw"`(default — 部署作者写错要立刻知道)aggregates ALL issues
73
73
  * into one error; `"skip"` drops them (pair with {@link validatePermissionRules} for a report, and/or
74
- * supply {@link onIssue}). Exception(codex 实现审 M3):`invalid.cap_exceeded` on the WHOLE rule set
74
+ * supply {@link onIssue}). Exception(实现审 M3):`invalid.cap_exceeded` on the WHOLE rule set
75
75
  * (maxRules) throws EVEN under `"skip"` — skipping it would compile an EMPTY table and fail-open. */
76
76
  onInvalidRule?: "throw" | "skip";
77
77
  /** Called once per dropped rule under `"skip"` (audit surface — silent drops are how a deployment
78
78
  * discovers, months later, that its deny never existed). Default: one aggregated `console.warn`. */
79
79
  onIssue?: (issue: PermissionRuleIssue) => void;
80
80
  /** What the policy returns when NO rule matches. Default `"allow"` = 本 policy 无意见(组合语义)。
81
- * default-closed 部署用 `"ask"`/`"deny"` —— 必须在这 INSIDE the policy(codex 127 审 B3:链尾
81
+ * default-closed 部署用 `"ask"`/`"deny"` —— 必须在这 INSIDE the policy(链尾
82
82
  * ask policy 会在 fold 里吞掉本 policy 的显式 allow)。 */
83
83
  defaultAction?: "allow" | "ask" | "deny";
84
84
  /** 已知主字段参数规则(`Bash(command:…)` 等)的处置。默认 `"reject"`(整串 lexical ≠ 前缀/AST,
@@ -136,7 +136,7 @@ export declare function namespacedRuleNameCovers(ruleName: string, toolName: str
136
136
  export declare function isNamespacedCoveringRuleName(name: string): boolean;
137
137
  /** Exported for the lockstep guard only (see {@link FILE_TOOL_PARAMS}) — not part of the rule DSL. */
138
138
  export declare const fileToolParamVocabulary: (canonicalTool: string) => ReadonlySet<string> | undefined;
139
- /** Dry-run 校验/迁移报告(codex 127 审 B5):不 throw,返回全部 issues(含 `unsupported.*` 分类,
139
+ /** Dry-run 校验/迁移报告:不 throw,返回全部 issues(含 `unsupported.*` 分类,
140
140
  * 供从 CC settings 迁移的部署逐条对照)。 */
141
141
  export declare function validatePermissionRules(rules: PermissionRule[], opts?: Pick<PermissionRulePolicyOptions, "primaryFieldGeneric" | "caps" | "bashPrefixLane">): PermissionRuleIssue[];
142
142
  /**
@@ -145,7 +145,7 @@ export declare function validatePermissionRules(rules: PermissionRule[], opts?:
145
145
  * bare allow → `defaultAction`. `check()` is idempotent and side-effect-free (combinePolicies 契约);
146
146
  * all parsing/validation happens HERE, the hot path never re-parses.
147
147
  *
148
- * ⚠️ Composition note (codex 127 审 B3): an `allow` from this policy is "no objection", NOT a
148
+ * ⚠️ Composition note: an `allow` from this policy is "no objection", NOT a
149
149
  * cross-policy override — another policy's ask/deny still outranks it in the combinePolicies fold.
150
150
  * Express default-closed via `defaultAction`, never via a trailing always-ask policy.
151
151
  */