@sema-agent/core 7.34.0 → 7.35.1

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 (85) hide show
  1. package/CHANGELOG.md +156 -3
  2. package/dist/brain/errors.d.ts +23 -1
  3. package/dist/brain/errors.js +19 -2
  4. package/dist/brain/status-sink.d.ts +7 -1
  5. package/dist/brain/stream-shared.d.ts +3 -1
  6. package/dist/brain/stream-shared.js +16 -4
  7. package/dist/core/ask-origin.d.ts +34 -6
  8. package/dist/core/ask-origin.js +24 -23
  9. package/dist/core/ask-question.js +2 -1
  10. package/dist/core/checkpoint-store.d.ts +4 -0
  11. package/dist/core/gate-fold.js +2 -2
  12. package/dist/core/gate-lanes.js +5 -4
  13. package/dist/core/governance-codes.d.ts +17 -1
  14. package/dist/core/governance-codes.js +11 -1
  15. package/dist/core/hooks.d.ts +3 -0
  16. package/dist/core/message-utils.d.ts +2 -1
  17. package/dist/core/message-utils.js +2 -1
  18. package/dist/core/mode-policy.d.ts +3 -0
  19. package/dist/core/mode-policy.js +5 -0
  20. package/dist/core/permission-rule-model.d.ts +4 -0
  21. package/dist/core/permission-rule-model.js +4 -0
  22. package/dist/core/permission-rules.js +21 -1
  23. package/dist/core/persisted-rule-arms.d.ts +4 -1
  24. package/dist/core/persisted-rule-arms.js +4 -0
  25. package/dist/core/retired-keys.js +1 -1
  26. package/dist/core/runner/active-skill-scope.js +5 -4
  27. package/dist/core/runner/assemble-result.js +2 -2
  28. package/dist/core/runner/contracts.d.ts +26 -4
  29. package/dist/core/runner/denial-limit-arms.d.ts +6 -1
  30. package/dist/core/runner/denial-limit-arms.js +4 -4
  31. package/dist/core/runner/file-history-request.d.ts +26 -0
  32. package/dist/core/runner/file-history-request.js +90 -0
  33. package/dist/core/runner/permission-rule-lanes.d.ts +2 -1
  34. package/dist/core/runner/permission-rule-lanes.js +14 -11
  35. package/dist/core/runner/prepare-ask-lane.d.ts +1 -1
  36. package/dist/core/runner/prepare-ask-lane.js +2 -2
  37. package/dist/core/runner/prepare-file-history.d.ts +9 -3
  38. package/dist/core/runner/prepare-file-history.js +4 -90
  39. package/dist/core/runner/prepare-gate-stations.d.ts +4 -3
  40. package/dist/core/runner/prepare-gate-stations.js +15 -11
  41. package/dist/core/runner/prepare-inherited-gate.d.ts +8 -1
  42. package/dist/core/runner/prepare-inherited-gate.js +10 -4
  43. package/dist/core/runner/prepare-park-ask.d.ts +4 -2
  44. package/dist/core/runner/prepare-park-ask.js +5 -4
  45. package/dist/core/runner/prepare-policy-chain.d.ts +3 -2
  46. package/dist/core/runner/prepare-policy-chain.js +171 -175
  47. package/dist/core/runner/prepare-task.d.ts +3 -1
  48. package/dist/core/runner/prepare-task.js +9 -5
  49. package/dist/core/runner/run-brain-sinks.js +3 -1
  50. package/dist/core/runner/run-leg.js +5 -3
  51. package/dist/core/runner/run-notification-lane.d.ts +5 -0
  52. package/dist/core/runner/run-notification-lane.js +3 -2
  53. package/dist/core/runner/runtask.d.ts +3 -1
  54. package/dist/core/runner/runtask.js +20 -16
  55. package/dist/core/runner/session-mode-seats.d.ts +48 -0
  56. package/dist/core/runner/session-mode-seats.js +73 -0
  57. package/dist/core/runner/session-rule-policy.js +3 -2
  58. package/dist/core/secret-env.d.ts +30 -58
  59. package/dist/core/secret-env.js +149 -586
  60. package/dist/core/sensitive-path-policy.js +2 -1
  61. package/dist/core/store-contracts/checkpoint-store-contract.js +2 -1
  62. package/dist/core/tool-policy.d.ts +31 -6
  63. package/dist/core/tool-policy.js +35 -16
  64. package/dist/core/tool-registry.d.ts +36 -0
  65. package/dist/core/tool-registry.js +22 -1
  66. package/dist/core/tool-roster.d.ts +17 -2
  67. package/dist/core/tool-roster.js +11 -1
  68. package/dist/core/trace.d.ts +11 -0
  69. package/dist/engine/execution-env/node-execution-env.d.ts +3 -1
  70. package/dist/engine/execution-env/node-execution-env.js +5 -2
  71. package/dist/engine/harness/agent-harness.js +2 -1
  72. package/dist/engine/lsp/node-lsp-manager.js +1 -1
  73. package/dist/index.d.ts +4 -4
  74. package/dist/index.js +4 -4
  75. package/dist/stores/file/index.d.ts +5 -3
  76. package/dist/stores/file/session-store.d.ts +8 -2
  77. package/dist/stores/file/session-store.js +5 -5
  78. package/dist/tools/fs/file-read-check.d.ts +2 -1
  79. package/dist/tools/fs/file-read-check.js +15 -7
  80. package/dist/tools/fs/shell-read-check.d.ts +10 -2
  81. package/dist/tools/fs/shell-read-check.js +12 -8
  82. package/package.json +1 -1
  83. package/test/export-surface.snapshot.json +15 -1
  84. package/wire-changes/core@7.35.0.json +584 -0
  85. package/wire-changes/core@7.35.1.json +59 -0
@@ -443,6 +443,10 @@ export interface ModeArbitrationRow {
443
443
  readonly sandboxAdmission: boolean;
444
444
  /** A persisted allow rule may clear it. */
445
445
  readonly ruleMayClear: boolean;
446
+ /** The reference implementation's rule / safety check answers `ask` for it, so a hook's ask beside it is not the hook's
447
+ * forced decision: the call runs the full permission pipeline (the reference's `hookAskFloor`) — under `dontAsk`, a
448
+ * refusal rather than a person's question. */
449
+ readonly hookAskFloor: boolean;
446
450
  }
447
451
  /**
448
452
  * The mode × class table. `bypassImmune` is the one new column (nothing said it before); the classifier and
@@ -451,7 +455,9 @@ export interface ModeArbitrationRow {
451
455
  * `ruleMayClear` restates {@link ruleClearanceOf}'s conclusion per class (a real-approval bit is never rule-clearable).
452
456
  * `mode` is the permission mode's OWN question (`plan`'s two sentences, `decisionReason: "mode"`): the reference
453
457
  * implementation puts it to a person past every allow rule and never to an automatic lane, and its bypass conversion
454
- * releases it like any other non-immune ask.
458
+ * releases it like any other non-immune ask. `hookAskFloor` marks the classes the reference's rule / safety check answers
459
+ * `ask` for — an ask rule, a question the tool needs a person for, a dangerous removal, a protected or unresolvable write
460
+ * target, the session transcript, another's shared artifact.
455
461
  */
456
462
  export declare const MODE_ARBITRATION: {
457
463
  readonly content_question: {
@@ -459,120 +465,140 @@ export declare const MODE_ARBITRATION: {
459
465
  readonly classifierEligible: false;
460
466
  readonly sandboxAdmission: false;
461
467
  readonly ruleMayClear: false;
468
+ readonly hookAskFloor: true;
462
469
  };
463
470
  readonly hook: {
464
471
  readonly bypassImmune: true;
465
472
  readonly classifierEligible: false;
466
473
  readonly sandboxAdmission: false;
467
474
  readonly ruleMayClear: false;
475
+ readonly hookAskFloor: false;
468
476
  };
469
477
  readonly ancestor: {
470
478
  readonly bypassImmune: true;
471
479
  readonly classifierEligible: false;
472
480
  readonly sandboxAdmission: false;
473
481
  readonly ruleMayClear: false;
482
+ readonly hookAskFloor: false;
474
483
  };
475
484
  readonly unverifiable_delete: {
476
485
  readonly bypassImmune: true;
477
486
  readonly classifierEligible: true;
478
487
  readonly sandboxAdmission: false;
479
488
  readonly ruleMayClear: false;
489
+ readonly hookAskFloor: true;
480
490
  };
481
491
  readonly transcript_write: {
482
492
  readonly bypassImmune: false;
483
493
  readonly classifierEligible: true;
484
494
  readonly sandboxAdmission: false;
485
495
  readonly ruleMayClear: false;
496
+ readonly hookAskFloor: true;
486
497
  };
487
498
  readonly org_rule: {
488
499
  readonly bypassImmune: true;
489
500
  readonly classifierEligible: false;
490
501
  readonly sandboxAdmission: false;
491
502
  readonly ruleMayClear: false;
503
+ readonly hookAskFloor: false;
492
504
  };
493
505
  readonly governance_unavailable: {
494
506
  readonly bypassImmune: true;
495
507
  readonly classifierEligible: false;
496
508
  readonly sandboxAdmission: false;
497
509
  readonly ruleMayClear: false;
510
+ readonly hookAskFloor: false;
498
511
  };
499
512
  readonly rule_unreadable: {
500
513
  readonly bypassImmune: false;
501
514
  readonly classifierEligible: false;
502
515
  readonly sandboxAdmission: false;
503
516
  readonly ruleMayClear: false;
517
+ readonly hookAskFloor: false;
504
518
  };
505
519
  readonly ask_rule: {
506
520
  readonly bypassImmune: true;
507
521
  readonly classifierEligible: false;
508
522
  readonly sandboxAdmission: false;
509
523
  readonly ruleMayClear: false;
524
+ readonly hookAskFloor: true;
510
525
  };
511
526
  readonly denial_limit_fallback: {
512
527
  readonly bypassImmune: true;
513
528
  readonly classifierEligible: false;
514
529
  readonly sandboxAdmission: false;
515
530
  readonly ruleMayClear: false;
531
+ readonly hookAskFloor: false;
516
532
  };
517
533
  readonly operator_forced: {
518
534
  readonly bypassImmune: true;
519
535
  readonly classifierEligible: true;
520
536
  readonly sandboxAdmission: true;
521
537
  readonly ruleMayClear: false;
538
+ readonly hookAskFloor: false;
522
539
  };
523
540
  readonly write_protection_unresolved: {
524
541
  readonly bypassImmune: true;
525
542
  readonly classifierEligible: false;
526
543
  readonly sandboxAdmission: true;
527
544
  readonly ruleMayClear: false;
545
+ readonly hookAskFloor: true;
528
546
  };
529
547
  readonly probe: {
530
548
  readonly bypassImmune: false;
531
549
  readonly classifierEligible: true;
532
550
  readonly sandboxAdmission: false;
533
551
  readonly ruleMayClear: false;
552
+ readonly hookAskFloor: false;
534
553
  };
535
554
  readonly write_protection: {
536
555
  readonly bypassImmune: false;
537
556
  readonly classifierEligible: true;
538
557
  readonly sandboxAdmission: true;
539
558
  readonly ruleMayClear: false;
559
+ readonly hookAskFloor: true;
540
560
  };
541
561
  readonly tool_marks_mcp: {
542
562
  readonly bypassImmune: false;
543
563
  readonly classifierEligible: true;
544
564
  readonly sandboxAdmission: true;
545
565
  readonly ruleMayClear: false;
566
+ readonly hookAskFloor: false;
546
567
  };
547
568
  readonly tool_marks_a2a: {
548
569
  readonly bypassImmune: false;
549
570
  readonly classifierEligible: true;
550
571
  readonly sandboxAdmission: true;
551
572
  readonly ruleMayClear: false;
573
+ readonly hookAskFloor: false;
552
574
  };
553
575
  readonly policy_rra: {
554
576
  readonly bypassImmune: true;
555
577
  readonly classifierEligible: true;
556
578
  readonly sandboxAdmission: false;
557
579
  readonly ruleMayClear: false;
580
+ readonly hookAskFloor: false;
558
581
  };
559
582
  readonly artifact_shared_read: {
560
583
  readonly bypassImmune: false;
561
584
  readonly classifierEligible: false;
562
585
  readonly sandboxAdmission: false;
563
586
  readonly ruleMayClear: false;
587
+ readonly hookAskFloor: true;
564
588
  };
565
589
  readonly mode: {
566
590
  readonly bypassImmune: false;
567
591
  readonly classifierEligible: false;
568
592
  readonly sandboxAdmission: false;
569
593
  readonly ruleMayClear: false;
594
+ readonly hookAskFloor: false;
570
595
  };
571
596
  readonly ordinary: {
572
597
  readonly bypassImmune: false;
573
598
  readonly classifierEligible: true;
574
599
  readonly sandboxAdmission: true;
575
600
  readonly ruleMayClear: true;
601
+ readonly hookAskFloor: false;
576
602
  };
577
603
  };
578
604
  /** The fence over the arbitration table: `never` while every class has a row. */
@@ -613,8 +639,9 @@ export interface AskClassification {
613
639
  export declare function decisionAskClassesOf(d: Extract<PermissionResult, {
614
640
  action: "ask";
615
641
  }>): Set<AskClass>;
616
- /** Fold a class set onto one row: `bypassImmune` by OR (any immune member keeps the ask), the three permissive
617
- * switches by AND (one refusing member withholds the lane). An empty set is `ordinary`. */
642
+ /** Fold a class set onto one row: `bypassImmune` and `hookAskFloor` by OR (any immune member keeps the ask; any floored
643
+ * member floors a hook's ask), the three permissive switches by AND (one refusing member withholds the lane). An empty
644
+ * set is `ordinary`. */
618
645
  export declare function foldArbitrationRow(classes: ReadonlySet<AskClass>): ModeArbitrationRow;
619
646
  /**
620
647
  * The ONE class derivation: the decision half ({@link decisionAskClassesOf}) joined with the gate's own facts —
@@ -680,9 +707,10 @@ export type Arbitration = {
680
707
  classes: readonly AskClass[];
681
708
  };
682
709
  /**
683
- * The ONE mode read point. `dontAsk` refuses every ask but a hook's and an operator authority's (an org rule, an
684
- * operator's forced confirmation, unreadable governance — a mode stance may not settle an operator-mandated
685
- * gate); `bypassPermissions` refuses a write whose protection could not be judged (a person cannot be asked, and
710
+ * The ONE mode read point. `dontAsk` refuses every ask but an operator authority's (an org rule, an operator's forced
711
+ * confirmation, unreadable governance — a mode stance may not settle an operator-mandated gate) and a hook's that no
712
+ * member of the set floors (`hookAskFloor`: a hook's ask beside a rule's or a safety check's ask is refused, as the
713
+ * reference's full pipeline refuses it); `bypassPermissions` refuses a write whose protection could not be judged (a person cannot be asked, and
686
714
  * releasing would be looser than the allow the judge already refuses), keeps every immune class and releases the
687
715
  * rest; `acceptEdits` answers an ORDINARY ask itself on a call the caller vouches is one of the mode's file commands
688
716
  * (`call.fileCommand`, `accepted`); `plan`, `default` and `auto` hand every ask on (plan's own questions were raised by
@@ -192,26 +192,26 @@ export function isAskClass(v) {
192
192
  return ASK_CLASS_SET.has(v);
193
193
  }
194
194
  export const MODE_ARBITRATION = {
195
- content_question: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false },
196
- hook: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false },
197
- ancestor: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false },
198
- unverifiable_delete: { bypassImmune: true, classifierEligible: true, sandboxAdmission: false, ruleMayClear: false },
199
- transcript_write: { bypassImmune: false, classifierEligible: true, sandboxAdmission: false, ruleMayClear: false },
200
- org_rule: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false },
201
- governance_unavailable: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false },
202
- rule_unreadable: { bypassImmune: false, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false },
203
- ask_rule: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false },
204
- denial_limit_fallback: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false },
205
- operator_forced: { bypassImmune: true, classifierEligible: true, sandboxAdmission: true, ruleMayClear: false },
206
- write_protection_unresolved: { bypassImmune: true, classifierEligible: false, sandboxAdmission: true, ruleMayClear: false },
207
- probe: { bypassImmune: false, classifierEligible: true, sandboxAdmission: false, ruleMayClear: false },
208
- write_protection: { bypassImmune: false, classifierEligible: true, sandboxAdmission: true, ruleMayClear: false },
209
- tool_marks_mcp: { bypassImmune: false, classifierEligible: true, sandboxAdmission: true, ruleMayClear: false },
210
- tool_marks_a2a: { bypassImmune: false, classifierEligible: true, sandboxAdmission: true, ruleMayClear: false },
211
- policy_rra: { bypassImmune: true, classifierEligible: true, sandboxAdmission: false, ruleMayClear: false },
212
- artifact_shared_read: { bypassImmune: false, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false },
213
- mode: { bypassImmune: false, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false },
214
- ordinary: { bypassImmune: false, classifierEligible: true, sandboxAdmission: true, ruleMayClear: true },
195
+ content_question: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: true },
196
+ hook: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false },
197
+ ancestor: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false },
198
+ unverifiable_delete: { bypassImmune: true, classifierEligible: true, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: true },
199
+ transcript_write: { bypassImmune: false, classifierEligible: true, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: true },
200
+ org_rule: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false },
201
+ governance_unavailable: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false },
202
+ rule_unreadable: { bypassImmune: false, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false },
203
+ ask_rule: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: true },
204
+ denial_limit_fallback: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false },
205
+ operator_forced: { bypassImmune: true, classifierEligible: true, sandboxAdmission: true, ruleMayClear: false, hookAskFloor: false },
206
+ write_protection_unresolved: { bypassImmune: true, classifierEligible: false, sandboxAdmission: true, ruleMayClear: false, hookAskFloor: true },
207
+ probe: { bypassImmune: false, classifierEligible: true, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false },
208
+ write_protection: { bypassImmune: false, classifierEligible: true, sandboxAdmission: true, ruleMayClear: false, hookAskFloor: true },
209
+ tool_marks_mcp: { bypassImmune: false, classifierEligible: true, sandboxAdmission: true, ruleMayClear: false, hookAskFloor: false },
210
+ tool_marks_a2a: { bypassImmune: false, classifierEligible: true, sandboxAdmission: true, ruleMayClear: false, hookAskFloor: false },
211
+ policy_rra: { bypassImmune: true, classifierEligible: true, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false },
212
+ artifact_shared_read: { bypassImmune: false, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: true },
213
+ mode: { bypassImmune: false, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false },
214
+ ordinary: { bypassImmune: false, classifierEligible: true, sandboxAdmission: true, ruleMayClear: true, hookAskFloor: false },
215
215
  };
216
216
  export const RRA_EXPLAINERS = new Set(["unverifiable_delete", "transcript_write", "org_rule", "governance_unavailable", "rule_unreadable", "denial_limit_fallback", "ancestor", "artifact_shared_read"]);
217
217
  export const RELEASABLE_ASK_CLASSES = ASK_CLASSES.filter((c) => !MODE_ARBITRATION[c].bypassImmune);
@@ -248,15 +248,16 @@ export function decisionAskClassesOf(d) {
248
248
  export function foldArbitrationRow(classes) {
249
249
  if (classes.size === 0)
250
250
  return MODE_ARBITRATION.ordinary;
251
- let bypassImmune = false, classifierEligible = true, sandboxAdmission = true, ruleMayClear = true;
251
+ let bypassImmune = false, classifierEligible = true, sandboxAdmission = true, ruleMayClear = true, hookAskFloor = false;
252
252
  for (const c of classes) {
253
253
  const row = MODE_ARBITRATION[c];
254
254
  bypassImmune ||= row.bypassImmune;
255
255
  classifierEligible &&= row.classifierEligible;
256
256
  sandboxAdmission &&= row.sandboxAdmission;
257
257
  ruleMayClear &&= row.ruleMayClear;
258
+ hookAskFloor ||= row.hookAskFloor;
258
259
  }
259
- return { bypassImmune, classifierEligible, sandboxAdmission, ruleMayClear };
260
+ return { bypassImmune, classifierEligible, sandboxAdmission, ruleMayClear, hookAskFloor };
260
261
  }
261
262
  export function classifyAsk(d, facts) {
262
263
  const classes = decisionAskClassesOf({ ...d, foldedAskClasses: undefined });
@@ -332,7 +333,7 @@ export function arbitrateByMode(mode, c, disclosure, call) {
332
333
  const classes = ASK_CLASSES.filter((k) => c.classes.has(k));
333
334
  switch (mode) {
334
335
  case "dontAsk":
335
- return has("hook") || has("org_rule") || has("operator_forced") || has("governance_unavailable") ? { kind: "ask" } : { kind: "denied_by_mode", mode, askClass: c.askClass, classes };
336
+ return (has("hook") && !c.row.hookAskFloor) || has("org_rule") || has("operator_forced") || has("governance_unavailable") ? { kind: "ask" } : { kind: "denied_by_mode", mode, askClass: c.askClass, classes };
336
337
  case "bypassPermissions":
337
338
  if (has("write_protection_unresolved"))
338
339
  return { kind: "denied_by_mode", mode, askClass: c.askClass, classes };
@@ -5,6 +5,7 @@ import { defineTool, errorResult } from "./tools.js";
5
5
  import { delimitUntrusted, inlineUntrusted } from "./untrusted-text.js";
6
6
  import { toolFace } from "./tool-catalog-entries.js";
7
7
  import { thrownText } from "./thrown-value.js";
8
+ import { callAnswersTo } from "./tool-roster.js";
8
9
  export const ASK_USER_QUESTION_TOOL_NAME = "AskUserQuestion";
9
10
  export function classifyQuestionOutcome(outcome) {
10
11
  let unavailable;
@@ -134,7 +135,7 @@ export function validateAskQuestions(questions) {
134
135
  export function createDurableQuestionPolicy() {
135
136
  return {
136
137
  check(req) {
137
- if (req.toolName !== ASK_USER_QUESTION_TOOL_NAME) {
138
+ if (!callAnswersTo(req, ASK_USER_QUESTION_TOOL_NAME)) {
138
139
  return { action: "allow" };
139
140
  }
140
141
  return { action: "ask", message: "AskUserQuestion: awaiting a human answer (durable)" };
@@ -868,6 +868,10 @@ export type PendingAction = {
868
868
  */
869
869
  family?: import("./tool-face.js").ToolFamily;
870
870
  pathTarget?: import("./tool-face.js").ToolRosterPathTarget;
871
+ /** The names a rule may spell the parked tool by (`ToolCallFace.names`: the wire name, then the aliases that resolve
872
+ * to it) off the same roster row at the same mint — the pre-CAS recheck judges an edit against rules written on
873
+ * either name, as the live gate judged the original. Absent on a row minted before it ⇒ the row's `toolName` alone. */
874
+ names?: readonly string[];
871
875
  /**
872
876
  * #438 (additive; no checkpoint-version bump — the `reminderMark` precedent: an optional field an
873
877
  * older reader ignores. Unlike that precedent seat — which resume DOES read, through its
@@ -50,7 +50,7 @@ export async function runGateFold(pass) {
50
50
  }
51
51
  if ((pass.decision.action === "allow" || pass.decision.action === "ask") && input.shellReadCheck !== undefined) {
52
52
  const station = input.shellReadCheck;
53
- const judged = station.judge(toolName, pass.policyRewrite !== undefined ? pass.policyRewrite : pass.currentInput, input.trackedCwd?.(), pass.callSignal ?? input.abortSignal, { toolCallId });
53
+ const judged = station.judge(pass.req, pass.policyRewrite !== undefined ? pass.policyRewrite : pass.currentInput, input.trackedCwd?.(), pass.callSignal ?? input.abortSignal, { toolCallId });
54
54
  if (judged !== null) {
55
55
  let verdict;
56
56
  try {
@@ -61,7 +61,7 @@ export async function runGateFold(pass) {
61
61
  if (kind !== "aborted" && input.abortSignal?.aborted !== true) {
62
62
  traceHookCrash(input, new Error(`shell read station ${kind === "timeout" ? "timed out" : "threw"} for tool "${toolName}" (toolCallId ${toolCallId}) — where this call reads could not be established: ${thrownText(err)}`), notifier);
63
63
  }
64
- verdict = station.unanswered(toolName, kind === "timeout" ? "the read station timed out" : "the read station could not answer");
64
+ verdict = station.unanswered(pass.req, kind === "timeout" ? "the read station timed out" : "the read station could not answer");
65
65
  }
66
66
  applyShellReadVerdict(pass, verdict, toolName, input.shellGated === true);
67
67
  }
@@ -11,6 +11,7 @@ import { CD_GIT_COMPOUND_SENTENCE, changesDirectoryBeforeGit, readOnlyShellVerdi
11
11
  import { readShellCommand } from "./shell-lexer.js";
12
12
  import { COMMAND_RULE_TOOL } from "./permission-rule-model.js";
13
13
  import { catalogRuleFaceOf } from "./tool-registry.js";
14
+ import { callAnswersTo, callFaceSeat } from "./tool-roster.js";
14
15
  import { modeAcceptsFileCommand } from "./mode-policy.js";
15
16
  import { ORG_ADJUDICATION_TIMEOUT_MS, ORG_RULE_DECISION_REASON, ORG_UNAVAILABLE_DECISION_REASON, settleOrgVerdictWithin } from "./permission-rule-org.js";
16
17
  import { exitGate } from "./runner/gate-exit.js";
@@ -169,7 +170,7 @@ export async function runGateLanes(pass) {
169
170
  writeProtectionUnresolved: pass.writeProtectionUnresolved === true,
170
171
  });
171
172
  const askOriginFacts = (org, ruleCallUnreadable) => ({
172
- contentQuestion: toolName === ASK_USER_QUESTION_TOOL_NAME,
173
+ contentQuestion: callAnswersTo(pass.req, ASK_USER_QUESTION_TOOL_NAME),
173
174
  markedUnresolvable: markedUnresolvable(),
174
175
  org,
175
176
  ...(ruleCallUnreadable ? { ruleCallUnreadable } : {}),
@@ -199,7 +200,7 @@ export async function runGateLanes(pass) {
199
200
  let decided = current;
200
201
  let pending;
201
202
  try {
202
- pending = Promise.resolve(input.orgRules.adjudicate({ toolName, args, toolCallId }));
203
+ pending = Promise.resolve(input.orgRules.adjudicate({ toolName, args, toolCallId, ...callFaceSeat(pass.req.face) }));
203
204
  }
204
205
  catch (err) {
205
206
  pending = Promise.resolve({ status: "unavailable", disclosures: [`the org adjudication face threw before answering: ${describeThrown(err)}`] });
@@ -767,14 +768,14 @@ export async function runGateLanes(pass) {
767
768
  }
768
769
  if (editDenied === undefined && input.shellReadCheck !== undefined) {
769
770
  const station = input.shellReadCheck;
770
- const judged = station.judge(toolName, editArgs, input.trackedCwd?.(), callSignal ?? input.abortSignal, { seat: "rejudge", toolCallId });
771
+ const judged = station.judge(pass.req, editArgs, input.trackedCwd?.(), callSignal ?? input.abortSignal, { seat: "rejudge", toolCallId });
771
772
  if (judged !== null) {
772
773
  let verdict;
773
774
  try {
774
775
  verdict = await withGateTimeout(judged, boundedAwaitDeadlineMs(), callSignal ?? input.abortSignal, "shellReadStation");
775
776
  }
776
777
  catch {
777
- verdict = station.unanswered(toolName, "the read station could not answer");
778
+ verdict = station.unanswered(pass.req, "the read station could not answer");
778
779
  }
779
780
  if (verdict.disposition !== "vouch") {
780
781
  editDenied = {
@@ -12,7 +12,6 @@ export declare const GOVERNANCE_CODES: {
12
12
  readonly "config.compliance_required": "transient";
13
13
  readonly "config.compliance_denied": "terminal";
14
14
  readonly "config.locked_key": "terminal";
15
- readonly "config.tool_mount_denied": "terminal";
16
15
  readonly "memory.capture_optout_denied": "terminal";
17
16
  };
18
17
  export type GovernanceCode = keyof typeof GOVERNANCE_CODES;
@@ -42,6 +41,23 @@ export declare const NON_GOVERNANCE_MEMORY_CODES: ReadonlySet<string>;
42
41
  * `undefined` (NOT governance — the caller falls through to its ordinary taxonomy).
43
42
  */
44
43
  export declare function governanceRetryClass(code: string): GovernanceRetryClass | undefined;
44
+ /**
45
+ * The CLOSED set of codes the `spec.tools` DECLARATION door refuses a caller's tool entry with, at prepare,
46
+ * before anything mounts: the entry cannot be built from its own members (`config.tool_mount_denied`), a
47
+ * member over a roster row's wire bound (`config.tool_roster_bound` / `config.tool_name_too_long` /
48
+ * `config.tool_contract_too_long`), a schema over the row's key bounds (`config.tool_schema_bound`), a
49
+ * declared face or result card the schema / namespace cannot carry (`config.tool_face_invalid`), a name or
50
+ * alias in the engine's protocol namespace (`config.tool_name_invalid`), an egress tool declared other than
51
+ * `write` (`config.egress_requires_write_effect` — also minted, same meaning, for a protocol tool's resolved
52
+ * axes), a name the run reserves for a tool the engine injects (`config.reserved_tool_name` — structured output,
53
+ * the skill tool, tool search, the offload reader, an injected protocol tool; `runGoal`'s completion tool, which
54
+ * refuses before any run). Each ends the run `failed` with the code on `terminal.code`. A caller-shape refusal, not a policy
55
+ * verdict: none of these is a governance code ({@link GOVERNANCE_CODES}); re-issuing the same declaration
56
+ * changes nothing. Every mint site at the door types its code against this set.
57
+ */
58
+ export declare const TOOL_DECLARATION_CODES: readonly ["config.tool_mount_denied", "config.tool_roster_bound", "config.tool_name_too_long", "config.tool_contract_too_long", "config.tool_schema_bound", "config.tool_face_invalid", "config.tool_name_invalid", "config.egress_requires_write_effect", "config.reserved_tool_name"];
59
+ /** A code in {@link TOOL_DECLARATION_CODES}. */
60
+ export type ToolDeclarationCode = (typeof TOOL_DECLARATION_CODES)[number];
45
61
  /**
46
62
  * The CLOSED registry of reasons a permission-rule sync round may refuse an inbound record or move a
47
63
  * local row into the quarantine area (design/182 §4.3/§5.2/§8.1). Same doctrine as
@@ -5,7 +5,6 @@ export const GOVERNANCE_CODES = {
5
5
  "config.compliance_required": "transient",
6
6
  "config.compliance_denied": "terminal",
7
7
  "config.locked_key": "terminal",
8
- "config.tool_mount_denied": "terminal",
9
8
  "memory.capture_optout_denied": "terminal",
10
9
  };
11
10
  export const NON_GOVERNANCE_MEMORY_CODES = new Set([
@@ -93,6 +92,17 @@ export function governanceRetryClass(code) {
93
92
  return "terminal";
94
93
  return undefined;
95
94
  }
95
+ export const TOOL_DECLARATION_CODES = [
96
+ "config.tool_mount_denied",
97
+ "config.tool_roster_bound",
98
+ "config.tool_name_too_long",
99
+ "config.tool_contract_too_long",
100
+ "config.tool_schema_bound",
101
+ "config.tool_face_invalid",
102
+ "config.tool_name_invalid",
103
+ "config.egress_requires_write_effect",
104
+ "config.reserved_tool_name",
105
+ ];
96
106
  export const RULE_SYNC_DROP_CODES = {
97
107
  invalid_rule_text: "inbound-refused-or-local-quarantined",
98
108
  metadata_mismatch: "inbound-refused-or-local-quarantined",
@@ -1111,6 +1111,9 @@ export interface ToolGateInput {
1111
1111
  * isolation scope (onHookError itself, the deny observer, the durable-park observer). The runner
1112
1112
  * wires this to an `observer.notify_failed` trace event; absent = counted only (unchanged). */
1113
1113
  onNotifyError?: (failure: import("./safe-notify.js").SafeNotifyFailure) => void;
1114
+ /** The call. `toolName` is the name every policy, hook and name-keyed table of this gate reads — the wire name of the
1115
+ * tool the call dispatches to: the Runner's station resolves a call made under an alias through the roster
1116
+ * (`rosterToolName`) before this gate runs; a host driving the gate resolves it the same way. */
1114
1117
  event: {
1115
1118
  toolCallId: string;
1116
1119
  toolName: string;
@@ -10,7 +10,8 @@ export declare function messageRole(m: AgentMessage): string | undefined;
10
10
  * same terms rather than on four. The READ twin of `noteResponseModel` (`brain/stream-shared.ts`), which
11
11
  * states the same law on the write side; this side re-tests rather than trusting, because a HOST's own
12
12
  * Brain mints these messages too. `undefined` ⇒ the provider named the stamped id, named nothing, or
13
- * named something unreadable — ONE reading, never three.
13
+ * named something unreadable — ONE reading, never three. Readable = {@link isProviderEchoShape}, the write
14
+ * twin's own predicate.
14
15
  */
15
16
  export declare function providerReportedModel(m: {
16
17
  readonly model?: unknown;
@@ -1,9 +1,10 @@
1
+ import { isProviderEchoShape } from "../brain/errors.js";
1
2
  export function messageRole(m) {
2
3
  return m.role;
3
4
  }
4
5
  export function providerReportedModel(m) {
5
6
  const reported = m.responseModel;
6
- if (typeof reported !== "string" || reported === "")
7
+ if (typeof reported !== "string" || !isProviderEchoShape(reported))
7
8
  return undefined;
8
9
  return reported === m.model ? undefined : reported;
9
10
  }
@@ -172,6 +172,9 @@ export declare class PermissionModeController {
172
172
  get current(): PermissionMode;
173
173
  /** The three-member answer as it stands right now. */
174
174
  receipt(): PermissionModeReceipt;
175
+ /** The ceiling's source moved to another leg (the parent session's next leg); `before` is this leg's receipt under the
176
+ * previous one. A ceiling fold the move produced — or changed — is announced, as the construction-time fold is. */
177
+ ceilingRebound(before: PermissionModeReceipt): void;
175
178
  /** Bind the deployment's arming facts (once, after the caps resolved); announces a fold-back if one results. */
176
179
  bindArming(arming: PermissionModeArming): PermissionModeReceipt;
177
180
  /** A steer: the requested word changes; the arming fold is re-judged; a leg that became `auto` arms its classifier.
@@ -136,6 +136,11 @@ export class PermissionModeController {
136
136
  return { ...base, effective: declared, foldedBy: "inherited_ceiling" };
137
137
  return { ...base, effective: declared };
138
138
  }
139
+ ceilingRebound(before) {
140
+ const after = this.receipt();
141
+ if (after.foldedBy === "inherited_ceiling" && (before.foldedBy !== "inherited_ceiling" || before.effective !== after.effective))
142
+ this.onFold?.(after);
143
+ }
139
144
  bindArming(arming) {
140
145
  const before = this.receipt();
141
146
  this.arming = arming;
@@ -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.
@@ -4,6 +4,7 @@ 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";
9
10
  import { readOnlyLineObjection, readOnlyShellVerdict } from "./read-only-shell.js";
@@ -30,6 +31,9 @@ export function ruleToolGrammarOf(toolName) {
30
31
  return "command";
31
32
  return pathTargetOf(toolName) !== undefined ? "path" : undefined;
32
33
  }
34
+ export function laneToolNamesOf(req) {
35
+ return ruleNamesOf(req).filter((n) => ruleToolGrammarOf(n) !== undefined);
36
+ }
33
37
  export const MAX_RULE_TEXT_CHARS = 512;
34
38
  export const BARE_INTERPRETER_NAMES = new Set([
35
39
  "node", "deno", "bun", "python", "python2", "python3", "perl", "ruby", "php", "osascript",
@@ -6,6 +6,7 @@ import { pathFamilyOf, pathIdentityOf, spellingOfIdentity } from "../tools/fs/sa
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 = {
@@ -379,11 +380,30 @@ export function createPermissionRulePolicy(rules, opts) {
379
380
  ruleUnreadable: lane,
380
381
  requiresRealApproval: true,
381
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
+ };
382
402
  return {
383
403
  nameSets: [nameSets],
384
404
  ...(pathDenyRules.length > 0 ? { pathDenyRules } : {}),
385
405
  check(req) {
386
- const entry = byTool.get(req.toolName);
406
+ const entry = entryFor(req);
387
407
  const covering = hasCovering && protocolOf(req.toolName) !== undefined;
388
408
  if (entry?.bare.deny) {
389
409
  return { action: "deny", message: ruleDenial(req, entry.bare.deny.ruleText, entry.bare.deny.source) };
@@ -12,7 +12,7 @@
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 {
@@ -23,6 +23,9 @@ export interface PersistedRuleRead {
23
23
  /** The lexer's reason when the CALL could not be read against the person's deny/ask rows. */
24
24
  readonly reason?: string;
25
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;
26
29
  /** Display form of a rule set (design/375 §5.1): every rule text is a DISPLAY value on a human trust
27
30
  * boundary, and the set must stay readable however large a lane makes it — each member rides
28
31
  * `inlineUntrusted` (200 cap, the same cap the durable leg's `RiskDescriptor.shadowedRule` carries),
@@ -1,5 +1,9 @@
1
1
  import { decisionAskClassesOf } from "./ask-origin.js";
2
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
+ }
3
7
  export function disclosedRuleSet(rules) {
4
8
  const shown = rules.slice(0, 5).map((r) => inlineUntrusted(r.rule, 200));
5
9
  const rest = rules.length - shown.length;
@@ -15,5 +15,5 @@ export function assertNoRetiredKeys(bag, table, owner) {
15
15
  }
16
16
  export const RETIRED_TASK_SPEC_KEYS = Object.freeze({
17
17
  autoModeRequested: 'the permission mode is one key now — write `permissionMode: "auto"`',
18
- approverPosture: 'the permission mode is one key now — write `permissionMode: "bypassPermissions"` (the engine renders the posture from the effective mode)',
18
+ approverPosture: 'a posture never authorized anything; drop the key (the engine renders the posture from the effective mode). Releasing asks is a separate decision: `permissionMode: "bypassPermissions"`',
19
19
  });
@@ -3,6 +3,7 @@ import { pathTargetValue } from "../tool-registry.js";
3
3
  import { pathTargetBaseOf } from "../effective-path-target.js";
4
4
  import { PATH_WRITE_TOOLS, isWithin } from "./session-rule-policy.js";
5
5
  import { parseSkillToolEntry, skillSpecifierRejection } from "../skill-tool-specifier.js";
6
+ import { ruleNamesOf } from "../tool-roster.js";
6
7
  export class ActiveSkillScope {
7
8
  frames = [];
8
9
  push(frame) {
@@ -22,7 +23,7 @@ export function createActiveSkillScopePolicy(opts) {
22
23
  const { scope, env, rootPath, toolEffects } = opts;
23
24
  return {
24
25
  async check(req, signal) {
25
- const toolName = req.toolName;
26
+ const names = ruleNamesOf(req);
26
27
  const frames = scope.active();
27
28
  if (frames.length === 0)
28
29
  return { action: "allow" };
@@ -47,7 +48,7 @@ export function createActiveSkillScopePolicy(opts) {
47
48
  }
48
49
  return { manifest: m, byName };
49
50
  });
50
- if (perFrame.length === 0 || perFrame.some((f) => !f.byName.has(toolName))) {
51
+ if (perFrame.length === 0 || perFrame.some((f) => !names.some((n) => f.byName.has(n)))) {
51
52
  const ids = manifests.map((m) => m.lineageId).join(", ");
52
53
  return {
53
54
  action: "deny",
@@ -56,7 +57,7 @@ export function createActiveSkillScopePolicy(opts) {
56
57
  };
57
58
  }
58
59
  for (const frame of perFrame) {
59
- const alternatives = frame.byName.get(toolName) ?? [];
60
+ const alternatives = names.flatMap((n) => frame.byName.get(n) ?? []);
60
61
  const rejections = [];
61
62
  let admitted = false;
62
63
  for (const entry of alternatives) {
@@ -79,7 +80,7 @@ export function createActiveSkillScopePolicy(opts) {
79
80
  const declaredTarget = req.face?.pathTarget;
80
81
  const skillScopeEligible = declaredTarget !== undefined && declaredTarget.skillScopeEligible === true;
81
82
  if (pathConstrainingActive && !skillScopeEligible) {
82
- const eff = toolEffects?.get(toolName) ?? "write";
83
+ const eff = toolEffects?.get(req.toolName) ?? "write";
83
84
  if (eff !== "read") {
84
85
  const ids = manifests.filter((m) => m.allowPaths?.length).map((m) => m.lineageId).join(", ");
85
86
  return {