@sema-agent/core 7.45.0 → 7.47.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 (139) hide show
  1. package/CHANGELOG.md +298 -1
  2. package/dist/agents/extra-tool-names.d.ts +65 -0
  3. package/dist/agents/extra-tool-names.js +124 -0
  4. package/dist/agents/subagent.d.ts +17 -5
  5. package/dist/agents/subagent.js +33 -25
  6. package/dist/agents/tool-filter.d.ts +17 -1
  7. package/dist/agents/tool-filter.js +7 -0
  8. package/dist/core/agent-config-dir.d.ts +20 -0
  9. package/dist/core/agent-config-dir.js +49 -0
  10. package/dist/core/allow-entry.d.ts +6 -4
  11. package/dist/core/allow-entry.js +6 -5
  12. package/dist/core/ask-origin.d.ts +35 -12
  13. package/dist/core/ask-origin.js +24 -7
  14. package/dist/core/checkpoint-store.d.ts +39 -2
  15. package/dist/core/effective-path-target.d.ts +136 -4
  16. package/dist/core/effective-path-target.js +110 -2
  17. package/dist/core/engine-notice.d.ts +12 -4
  18. package/dist/core/gate-fold.js +5 -2
  19. package/dist/core/gate-lanes.js +11 -4
  20. package/dist/core/gate-outcome.d.ts +4 -2
  21. package/dist/core/gate-outcome.js +2 -2
  22. package/dist/core/governance-codes.d.ts +3 -1
  23. package/dist/core/governance-codes.js +3 -0
  24. package/dist/core/hooks.d.ts +3 -0
  25. package/dist/core/path-rule-binding.d.ts +65 -0
  26. package/dist/core/path-rule-binding.js +99 -0
  27. package/dist/core/permission-rule-model.d.ts +59 -24
  28. package/dist/core/permission-rule-model.js +48 -44
  29. package/dist/core/permission-rule-provider.d.ts +7 -0
  30. package/dist/core/permission-rule-provider.js +10 -3
  31. package/dist/core/permission-rule-store.d.ts +8 -0
  32. package/dist/core/permission-rule-store.js +6 -2
  33. package/dist/core/permission-rule-sync.js +2 -2
  34. package/dist/core/permission-rules.d.ts +26 -45
  35. package/dist/core/permission-rules.js +77 -57
  36. package/dist/core/runner/child-tree.d.ts +97 -0
  37. package/dist/core/runner/child-tree.js +104 -0
  38. package/dist/core/runner/construction-doors.d.ts +5 -0
  39. package/dist/core/runner/construction-doors.js +6 -0
  40. package/dist/core/runner/contracts.d.ts +46 -0
  41. package/dist/core/runner/gate-exit.d.ts +4 -1
  42. package/dist/core/runner/gate-exit.js +5 -1
  43. package/dist/core/runner/permission-rule-lanes.d.ts +34 -25
  44. package/dist/core/runner/permission-rule-lanes.js +18 -12
  45. package/dist/core/runner/persisted-path-rows.d.ts +31 -7
  46. package/dist/core/runner/persisted-path-rows.js +31 -12
  47. package/dist/core/runner/prepare-ask-lane.d.ts +4 -0
  48. package/dist/core/runner/prepare-ask-lane.js +4 -2
  49. package/dist/core/runner/prepare-execution-env.d.ts +5 -2
  50. package/dist/core/runner/prepare-execution-env.js +10 -3
  51. package/dist/core/runner/prepare-gate-stations.d.ts +3 -0
  52. package/dist/core/runner/prepare-gate-stations.js +2 -1
  53. package/dist/core/runner/prepare-hands-readface.d.ts +8 -4
  54. package/dist/core/runner/prepare-hands-readface.js +11 -11
  55. package/dist/core/runner/prepare-inherited-gate.d.ts +8 -1
  56. package/dist/core/runner/prepare-inherited-gate.js +8 -2
  57. package/dist/core/runner/prepare-park-ask.d.ts +3 -0
  58. package/dist/core/runner/prepare-park-ask.js +5 -3
  59. package/dist/core/runner/prepare-policy-chain.d.ts +14 -2
  60. package/dist/core/runner/prepare-policy-chain.js +13 -5
  61. package/dist/core/runner/prepare-suspend-saga.d.ts +10 -0
  62. package/dist/core/runner/prepare-suspend-saga.js +7 -1
  63. package/dist/core/runner/prepare-task.js +37 -18
  64. package/dist/core/runner/prepare-turn-wiring.d.ts +2 -0
  65. package/dist/core/runner/prepare-turn-wiring.js +2 -1
  66. package/dist/core/runner/prepare-workspace-restore.d.ts +3 -0
  67. package/dist/core/runner/prepare-workspace-restore.js +1 -0
  68. package/dist/core/runner/resume-apply.js +17 -3
  69. package/dist/core/runner/resume-checkpoint-screen.js +13 -0
  70. package/dist/core/runner/run-leg.js +5 -1
  71. package/dist/core/runner/runtask.js +2 -2
  72. package/dist/core/runner/sub-agent-deny-lists.d.ts +1 -1
  73. package/dist/core/runner/write-protection-station.d.ts +3 -2
  74. package/dist/core/runner/write-protection-station.js +1 -0
  75. package/dist/core/runner-deps.d.ts +14 -0
  76. package/dist/core/sensitive-path-policy.d.ts +8 -0
  77. package/dist/core/sensitive-path-policy.js +18 -2
  78. package/dist/core/shell-wrapper-projection.d.ts +11 -0
  79. package/dist/core/shell-wrapper-projection.js +56 -0
  80. package/dist/core/shell-wrapper-table.d.ts +144 -34
  81. package/dist/core/shell-wrapper-table.js +40 -20
  82. package/dist/core/synthetic-read-verdict.d.ts +3 -0
  83. package/dist/core/synthetic-read-verdict.js +3 -1
  84. package/dist/core/tool-catalog-entries.d.ts +21 -0
  85. package/dist/core/tool-catalog-entries.js +12 -2
  86. package/dist/core/tool-face.d.ts +10 -1
  87. package/dist/core/tool-name-readings.d.ts +8 -0
  88. package/dist/core/tool-name-readings.js +2 -0
  89. package/dist/core/tool-policy.d.ts +40 -17
  90. package/dist/core/tool-policy.js +11 -9
  91. package/dist/core/tool-registry.d.ts +9 -0
  92. package/dist/core/tool-registry.js +10 -0
  93. package/dist/core/tool-spec.d.ts +3 -2
  94. package/dist/core/write-protect.d.ts +14 -3
  95. package/dist/core/write-protect.js +41 -10
  96. package/dist/index.d.ts +6 -4
  97. package/dist/index.js +5 -3
  98. package/dist/orchestration/run-spec.js +8 -7
  99. package/dist/orchestration/workflow.js +1 -1
  100. package/dist/shell/lex.d.ts +22 -0
  101. package/dist/shell/lex.js +75 -13
  102. package/dist/tools/fs/bash-program-position.d.ts +14 -90
  103. package/dist/tools/fs/bash-program-position.js +41 -74
  104. package/dist/tools/fs/bash-readonly-classifier.d.ts +17 -444
  105. package/dist/tools/fs/bash-readonly-classifier.js +169 -1225
  106. package/dist/tools/fs/index.d.ts +3 -2
  107. package/dist/tools/fs/program-read-model.d.ts +711 -0
  108. package/dist/tools/fs/program-read-model.js +1259 -0
  109. package/dist/tools/fs/program-read-model.rows.d.ts +18 -0
  110. package/dist/tools/fs/program-read-model.rows.js +624 -0
  111. package/dist/tools/fs/read-deny-home.d.ts +19 -1
  112. package/dist/tools/fs/read-deny-home.js +62 -24
  113. package/dist/tools/fs/read-deny.d.ts +54 -14
  114. package/dist/tools/fs/read-deny.js +104 -50
  115. package/dist/tools/fs/removal-targets.d.ts +7 -6
  116. package/dist/tools/fs/removal-targets.js +34 -24
  117. package/dist/tools/fs/safety.d.ts +3 -0
  118. package/dist/tools/fs/safety.js +10 -3
  119. package/dist/tools/fs/shell-read-argv.d.ts +86 -0
  120. package/dist/tools/fs/shell-read-argv.js +388 -0
  121. package/dist/tools/fs/shell-read-bindings.d.ts +300 -0
  122. package/dist/tools/fs/shell-read-bindings.js +755 -0
  123. package/dist/tools/fs/shell-read-check.js +23 -15
  124. package/dist/tools/fs/shell-read-establish.js +82 -8
  125. package/dist/tools/fs/shell-read-judge.d.ts +11 -1
  126. package/dist/tools/fs/shell-read-judge.js +9 -1
  127. package/dist/tools/fs/shell-read-plan.d.ts +33 -8
  128. package/dist/tools/fs/shell-read-plan.js +309 -407
  129. package/dist/tools/fs/shell-read-word.d.ts +60 -26
  130. package/dist/tools/fs/shell-read-word.js +47 -37
  131. package/dist/tools/fs/shell-read-write-targets.d.ts +15 -64
  132. package/dist/tools/fs/shell-read-write-targets.js +85 -137
  133. package/dist/tools/fs/shell-reentry-text.d.ts +136 -6
  134. package/dist/tools/fs/shell-reentry-text.js +203 -53
  135. package/dist/tools/monitor.d.ts +3 -3
  136. package/package.json +1 -1
  137. package/test/export-surface.snapshot.json +25 -5
  138. package/wire-changes/core@7.46.0.json +1522 -0
  139. package/wire-changes/core@7.47.0.json +412 -0
@@ -488,6 +488,9 @@ export interface ModeArbitrationRow {
488
488
  * tool's effect lands at a remote peer, outside any containment the leg declares). `hookAskFloor` marks the classes the reference's rule / safety check answers
489
489
  * `ask` for — an ask rule, a question the tool needs a person for, a dangerous removal, a protected or unresolvable write
490
490
  * target, the session transcript, another's shared artifact.
491
+ * A row is a class's reading on every face but the ones {@link RELEASED_ON_SHELL_FACE} names: `write_protection_unresolved`'s
492
+ * `bypassImmune` is the file face's (and every other face's) reading, and the shell face reads it released — folded where the
493
+ * ask's row is ({@link foldArbitrationRow}), listed per face by {@link releasableAskClassesFor}.
491
494
  */
492
495
  export declare const MODE_ARBITRATION: {
493
496
  readonly content_question: {
@@ -668,23 +671,37 @@ export declare const MODE_ARBITRATION: {
668
671
  };
669
672
  /** The fence over the arbitration table: `never` while every class has a row. */
670
673
  export type ArbitrationTableCoversEveryClass = AssertAllKeysHandled<Exclude<AskClass, keyof typeof MODE_ARBITRATION>>;
674
+ /**
675
+ * The FACE a question was put on — the second axis the permission mode reads a class under, beside the leg's read face
676
+ * ({@link ContainmentFace}). `shell`: the call's `command` argument is a shell command line the command grammar judges (the
677
+ * tool catalog's shell face — Bash, Monitor); `other`: every other call (the file tools, MCP, A2A, a caller's own tool). The set
678
+ * is closed; {@link askFaceOf} is the one derivation of a face from a tool name.
679
+ */
680
+ export declare const ASK_FACES: readonly ["shell", "other"];
681
+ export type AskFace = (typeof ASK_FACES)[number];
682
+ /** The face of a call, by its WIRE name (the name the gate resolved the call to — a release widens, so an alias a tool merely
683
+ * wears does not count): the catalog's shell face for exactly the names that declare the command grammar, `other` for every
684
+ * other name. */
685
+ export declare function askFaceOf(toolName: string): AskFace;
671
686
  /** The classes whose own mint puts the `requiresRealApproval` bit on the ask — a bit one of them explains is not
672
687
  * the bare bit `policy_rra` names. The mark classes (probe / write protection / tool marks / operator forced / the
673
688
  * shell floor) mint no bit and explain none — except `requires_user_interaction`, whose declaration the origin stamp
674
689
  * turns into the bit on every ask of the tool. */
675
690
  export declare const RRA_EXPLAINERS: ReadonlySet<AskClass>;
676
- /** The classes a permission mode can RELEASE (the `bypassImmune: false` rows) — the only words that can appear on
677
- * `GateDisposition.released.askClass`; every other class reaches a person under every mode. */
678
- export declare const RELEASABLE_ASK_CLASSES: readonly AskClass[];
691
+ /** The classes a permission mode can RELEASE for a call on `face`, in {@link ASK_CLASSES} order. The same list on every face
692
+ * but one class — {@link RELEASED_ON_SHELL_FACE}. A frozen list: a caller holding it cannot change the next read. A word
693
+ * outside {@link ASK_FACES} throws. */
694
+ export declare function releasableAskClassesFor(face: AskFace): readonly AskClass[];
679
695
  /**
680
696
  * The closed set of `reason` words a mode release carries (`GateDisposition.released.reason`, the `released` arm of
681
697
  * {@link Arbitration}): each is a releasable class the release record names on its own — `rule_unreadable`, a release
682
- * over a person's rule the call could not be read against. A release carries the first member (array order) its class
698
+ * over a person's rule the call could not be read against; `write_protection_unresolved`, a release (the shell face only,
699
+ * {@link RELEASED_ON_SHELL_FACE}) over a write target the judge could not resolve. A release carries the first member (array order) its class
683
700
  * set holds, and none when it holds no member. {@link arbitrateByMode} and {@link isModeReleaseReason} read one copy
684
701
  * of this list taken at load (a caller that writes into the exported array changes neither), so a new reason is one
685
702
  * word here.
686
703
  */
687
- export declare const MODE_RELEASE_REASONS: readonly ["rule_unreadable"];
704
+ export declare const MODE_RELEASE_REASONS: readonly ["rule_unreadable", "write_protection_unresolved"];
688
705
  export type ModeReleaseReason = (typeof MODE_RELEASE_REASONS)[number];
689
706
  /** Membership test for {@link ModeReleaseReason} — the word crosses the wire on `tool_end.gate.disposition.released.reason`. */
690
707
  export declare function isModeReleaseReason(v: unknown): v is ModeReleaseReason;
@@ -700,6 +717,8 @@ export interface AskClassFacts extends AskOriginFacts {
700
717
  readonly ancestorClasses?: ReadonlySet<AskClass>;
701
718
  /** The read face the leg this ask was raised on resolved ({@link ContainmentFace}); absent ⇒ none resolved (a leg with no hands). */
702
719
  readonly readFace?: ContainmentFace;
720
+ /** The tool face the question was put on ({@link askFaceOf} of the call's tool name); absent ⇒ the face-less reading, which is the `other` face's. */
721
+ readonly askFace?: AskFace;
703
722
  }
704
723
  /** The classification the arbitration reads: the whole set, the disclosed word (the set's first member in
705
724
  * {@link ASK_CLASSES} order) and the row folded across the set. */
@@ -735,10 +754,11 @@ export declare function decisionAskClassesOf(d: Extract<PermissionResult, {
735
754
  }>): Set<AskClass>;
736
755
  /** Fold a class set onto one row: `bypassImmune` and `hookAskFloor` by OR (any immune member keeps the ask; any floored
737
756
  * member floors a hook's ask), the three permissive switches by AND (one refusing member withholds the lane). An empty
738
- * set is `ordinary`. `readFace` is the face the ask was raised under: `roots` takes the classifier's lane from a set that
739
- * holds a class the narrowed face withholds from it — the one place the face enters, so every reader of `classifierEligible`
740
- * reads it already folded. */
741
- export declare function foldArbitrationRow(classes: ReadonlySet<AskClass>, readFace?: ContainmentFace): ModeArbitrationRow;
757
+ * set is `ordinary`. The two faces the ask was raised under enter HERE and nowhere else, so every reader of the folded row
758
+ * reads it already folded: `readFace` — `roots` takes the classifier's lane from a set that holds a class the narrowed face
759
+ * withholds from it; `askFace` — `shell` reads a class {@link RELEASED_ON_SHELL_FACE} holds as not bypass-immune. Absent
760
+ * faces read the table's own rows. */
761
+ export declare function foldArbitrationRow(classes: ReadonlySet<AskClass>, readFace?: ContainmentFace, askFace?: AskFace): ModeArbitrationRow;
742
762
  /**
743
763
  * The ONE class derivation: the decision half ({@link decisionAskClassesOf}) joined with the gate's own facts —
744
764
  * the question tool, an ancestor's mark, the org layer's two arms, the persisted lane's unreadable call, the tool
@@ -814,9 +834,12 @@ export type Arbitration = {
814
834
  * The ONE mode read point. `dontAsk` refuses every ask — one value for every class — but a hook's that no member of the
815
835
  * set floors (`hookAskFloor`: a hook's ask beside a rule's or a safety check's ask is refused, as the reference's full
816
836
  * pipeline refuses it; a hook's ask alone is the reference's forced decision, which never reaches that conversion);
817
- * `bypassPermissions` refuses a write whose protection could not be judged (a person cannot be asked, and
818
- * releasing would be looser than the allow the judge already refuses), keeps every immune class and releases the
819
- * rest; `acceptEdits` answers an ORDINARY ask itself on a call the caller vouches is one of the mode's file commands
837
+ * `bypassPermissions` refuses a write whose protection could not be judged where that class stays immune (the file
838
+ * face: a person cannot be asked, and releasing would be looser than the allow the write-protection tighten refuses there —
839
+ * the folded row says it, {@link foldArbitrationRow}), keeps every immune class and releases the rest: the shell face
840
+ * releases that class ({@link RELEASED_ON_SHELL_FACE}), and there the tighten turns an allow into the same question rather
841
+ * than a refusal (`writeProtectionTightenDecision` reads the face), so on neither face is the release looser than an allow;
842
+ * `acceptEdits` answers an ORDINARY ask itself on a call the caller vouches is one of the mode's file commands
820
843
  * (`call.fileCommand`, `accepted`); `plan`, `default` and `auto` hand every ask on (plan's own questions were raised by
821
844
  * its write gate; the reference implementation refuses nothing at this point under plan — auto's classifier reads the
822
845
  * row at its own station). Reads the class SET and the folded row — never the disclosed word.
@@ -1,3 +1,4 @@
1
+ import { isShellFaceToolName } from "./tool-registry.js";
1
2
  export const ASK_ORIGINS = Object.freeze(["content_question", "ancestor_marked", "org_unavailable", "org_rule", "rule_store_unavailable", "hook", "ask_rule", "denial_limit_fallback", "shell_gate_tighten", "safety_tighten", "policy"]);
2
3
  const ASK_ORIGIN_SET = new Set(ASK_ORIGINS);
3
4
  export function isAskOrigin(v) {
@@ -117,7 +118,7 @@ const MANDATE_DISCLOSURE = {
117
118
  write_protection_unresolved: "the write-protection judge could not resolve this write's target, so whether it lands on the protected table is unknown — it is cleared by confirming this call: no standing rule and no automatic decider answers for it, though a wired approver seat can",
118
119
  };
119
120
  export function mandateDisclosureOf(mandate) {
120
- return mandate === undefined ? "an explicit ask rule matched this call (a person's ask-me-each-time outranks a standing allow rule)" : MANDATE_DISCLOSURE[mandate];
121
+ return mandate === undefined ? "an explicit ask rule matched this call (a person's or the deployment's ask-me-each-time outranks a standing allow rule)" : MANDATE_DISCLOSURE[mandate];
121
122
  }
122
123
  export function persistedRuleMandateOf(marks) {
123
124
  const word = irreversibilityMandateOf(marks);
@@ -224,10 +225,26 @@ export const MODE_ARBITRATION = {
224
225
  unstated: { bypassImmune: false, classifierEligible: true, sandboxAdmission: false, ruleMayClear: true, hookAskFloor: false },
225
226
  ordinary: { bypassImmune: false, classifierEligible: true, sandboxAdmission: true, ruleMayClear: true, hookAskFloor: false },
226
227
  };
228
+ export const ASK_FACES = Object.freeze(["shell", "other"]);
229
+ export function askFaceOf(toolName) {
230
+ return isShellFaceToolName(toolName) ? "shell" : "other";
231
+ }
232
+ const RELEASED_ON_SHELL_FACE = new Set(["write_protection_unresolved"]);
233
+ function bypassImmuneOn(c, face) {
234
+ return MODE_ARBITRATION[c].bypassImmune && !(face === "shell" && RELEASED_ON_SHELL_FACE.has(c));
235
+ }
227
236
  const CLASSIFIER_WITHHELD_ON_ROOTS = new Set(["probe"]);
228
237
  export const RRA_EXPLAINERS = new Set(["dangerous_delete", "unverifiable_delete", "transcript_write", "org_rule", "governance_unavailable", "rule_unreadable", "denial_limit_fallback", "classifier_no_ruling", "ancestor", "artifact_shared_read", "requires_user_interaction"]);
229
- export const RELEASABLE_ASK_CLASSES = ASK_CLASSES.filter((c) => !MODE_ARBITRATION[c].bypassImmune);
230
- export const MODE_RELEASE_REASONS = Object.freeze(["rule_unreadable"]);
238
+ const RELEASABLE_BY_FACE = Object.freeze({
239
+ shell: Object.freeze(ASK_CLASSES.filter((c) => !bypassImmuneOn(c, "shell"))),
240
+ other: Object.freeze(ASK_CLASSES.filter((c) => !bypassImmuneOn(c, "other"))),
241
+ });
242
+ export function releasableAskClassesFor(face) {
243
+ if (!ASK_FACES.includes(face))
244
+ throw new TypeError(`releasableAskClassesFor: "${String(face)}" is not an ask face (${ASK_FACES.join(", ")})`);
245
+ return RELEASABLE_BY_FACE[face];
246
+ }
247
+ export const MODE_RELEASE_REASONS = Object.freeze(["rule_unreadable", "write_protection_unresolved"]);
231
248
  const MODE_RELEASE_REASON_SET = new Set(MODE_RELEASE_REASONS);
232
249
  export function isModeReleaseReason(v) {
233
250
  return MODE_RELEASE_REASON_SET.has(v);
@@ -278,14 +295,14 @@ export function decisionAskClassesOf(d) {
278
295
  out.add(c);
279
296
  return out;
280
297
  }
281
- export function foldArbitrationRow(classes, readFace) {
298
+ export function foldArbitrationRow(classes, readFace, askFace) {
282
299
  if (classes.size === 0)
283
300
  return MODE_ARBITRATION.ordinary;
284
301
  const withheld = readFace === "roots" && [...classes].some((c) => CLASSIFIER_WITHHELD_ON_ROOTS.has(c));
285
302
  let bypassImmune = false, classifierEligible = !withheld, sandboxAdmission = true, ruleMayClear = true, hookAskFloor = false;
286
303
  for (const c of classes) {
287
304
  const row = MODE_ARBITRATION[c];
288
- bypassImmune ||= row.bypassImmune;
305
+ bypassImmune ||= bypassImmuneOn(c, askFace);
289
306
  classifierEligible &&= row.classifierEligible;
290
307
  sandboxAdmission &&= row.sandboxAdmission;
291
308
  ruleMayClear &&= row.ruleMayClear;
@@ -340,7 +357,7 @@ export function classifyAsk(d, facts) {
340
357
  if (classes.size === 0)
341
358
  classes.add("ordinary");
342
359
  const askClass = ASK_CLASSES.find((c) => classes.has(c));
343
- return { classes, askClass, row: foldArbitrationRow(classes, facts.readFace) };
360
+ return { classes, askClass, row: foldArbitrationRow(classes, facts.readFace, facts.askFace) };
344
361
  }
345
362
  export function headlessDenialMessage(toolName, askMessage) {
346
363
  return `Permission for this tool use was denied: it requires interactive approval, and permission prompts are not available in this session (nobody can be asked to approve "${toolName}" in this run — headless auto-deny). The action was NOT performed. Do not claim it succeeded, and do not retry it in this session — report the limitation to the user, or suggest an alternative. What was requested: ${askMessage}`;
@@ -379,7 +396,7 @@ export function arbitrateByMode(mode, c, disclosure, call) {
379
396
  case "dontAsk":
380
397
  return has("hook") && !c.row.hookAskFloor ? { kind: "ask" } : { kind: "denied_by_mode", mode, askClass: c.askClass, classes };
381
398
  case "bypassPermissions":
382
- if (has("write_protection_unresolved"))
399
+ if (has("write_protection_unresolved") && c.row.bypassImmune)
383
400
  return { kind: "denied_by_mode", mode, askClass: c.askClass, classes };
384
401
  if (c.row.bypassImmune)
385
402
  return { kind: "ask" };
@@ -1095,6 +1095,22 @@ export interface CheckpointState {
1095
1095
  * plain on-disk state that survives a process-local suspend (worktrees included — the tree stays on disk).
1096
1096
  */
1097
1097
  handsCwd?: string;
1098
+ /**
1099
+ * The hands band's ROOT at suspend (the canonical root its tools resolve a root-relative spelling against — the
1100
+ * `root` every adjudicated request of the leg carries). Read by ONE seat: the pre-claim recheck of an edited parked
1101
+ * call, which has no environment yet and judges the edit at the row's coordinates (`handsRoot` + {@link handsCwd}).
1102
+ * After the claim the resumed leg uses its own live root. Absent on a row written before this field (that recheck then
1103
+ * reads the root as unknown, as before) and when the task has no hands. An ignorable optional field: an older reader
1104
+ * that does not know it resumes the row as it always did, so it raises no row format.
1105
+ */
1106
+ handsRoot?: string;
1107
+ /**
1108
+ * The home the leg's caller policies installed their `~/` words at (the execution environment's declared home, absolute),
1109
+ * beside {@link handsRoot}: the pre-claim recheck installs the edit-base policy at the row's root and this home, the way
1110
+ * the leg did. Absent on a row written before this field, on a hands-less task and on an environment that declares no
1111
+ * absolute home — a `~/` row then reads unanchored there (never the engine process's home). Ignorable, like `handsRoot`.
1112
+ */
1113
+ handsHome?: string;
1098
1114
  /**
1099
1115
  * the ACTIVE EnterWorktree session at suspend (worktree.ts keeps it in a shared
1100
1116
  * serializable ref, not a closure-only var, precisely so it lands here). Without it a resume LOST the
@@ -1166,6 +1182,21 @@ export interface CheckpointState {
1166
1182
  * the v2 contract are refused, even when every admitted set en route was EMPTY. Absent on
1167
1183
  * trees that never had a governance surface. */
1168
1184
  orgAdmissionGoverned?: true;
1185
+ /** The chain's top leg's root and home at suspend (data half): the anchors of the person's global / session path rows
1186
+ * and of every `~/` row on this chain (a project row anchors at its own scope root). Absent on older checkpoints and on
1187
+ * a top leg ⇒ the resumed leg is its own top. Rebased with the workspace on a restore elsewhere; a value that is not
1188
+ * an absolute path anchors nothing (the rows it would anchor read `unreadable`). */
1189
+ pathRuleAnchors?: {
1190
+ readonly root: string;
1191
+ readonly home?: string;
1192
+ };
1193
+ /** Where each parent constraint the suspended leg ran under was installed, position for position (data half; `null`: an
1194
+ * entry with no path rules). A resume that re-supplies the chain uninstalled installs each entry there. Rebased with the
1195
+ * workspace; a value that is not an absolute path installs nothing (the entry is installed at the chain top). */
1196
+ pathRuleInstallations?: ReadonlyArray<{
1197
+ readonly root: string;
1198
+ readonly home?: string;
1199
+ } | null>;
1169
1200
  requiresParentConstraint: boolean;
1170
1201
  /** How many opaque parent constraints the suspended leg ran under (present iff `requiresParentConstraint`).
1171
1202
  * The resume pre-CAS ladder rejects a re-supply whose `parentConstraints.length` differs
@@ -2036,14 +2067,20 @@ export declare function summarizeCheckpoint(cp: Checkpoint): CheckpointSummary;
2036
2067
  * steer seat, which no worker version reads and the host decides itself, or a retired mode-seed member of
2037
2068
  * `inheritedGate`, which an engine at 7.38.x or earlier still reads once).
2038
2069
  * · `invalid_outcome` pre-CAS row-integrity arms: `"real_approval_damaged"`, `"real_approval_forged"`,
2039
- * `"read_face_damaged"`, `"read_face_forged"`, `"write_face_damaged"`, `"skill_grants_damaged"`, `"constraint_chain_missing"` (all terminal for the row's
2070
+ * `"read_face_damaged"`, `"read_face_forged"`, `"write_face_damaged"`, `"skill_grants_damaged"`, `"constraint_chain_missing"`, `"hands_coordinates_damaged"` (all terminal for the row's
2040
2071
  * current bytes — no worker version redeems a damaged row).
2041
2072
  */
2042
2073
  export type CheckpointErrorReason = "version_newer" | "env_factory_missing" | "governed_unwired" | "origin_missing" | "ledger_event_unknown" | "seat_retired" | "real_approval_damaged" | "real_approval_forged" | "read_face_damaged" | "read_face_forged"
2043
2074
  /** `state.writeFace` is present but not a well-formed section. No `_forged` twin: the section is
2044
2075
  * legal-and-absent at every version, so there is no version to judge a fabrication against — and a
2045
2076
  * fabricated "open" still needs the LIVE resolution to say open too. */
2046
- | "write_face_damaged" | "skill_grants_damaged" | "constraint_chain_missing";
2077
+ | "write_face_damaged" | "skill_grants_damaged" | "constraint_chain_missing"
2078
+ /** `state.handsRoot` / `state.handsHome` / `state.handsCwd` is present but not an absolute path (the row's hands' coordinates). */
2079
+ | "hands_coordinates_damaged"
2080
+ /** On `resume.parent_constraint_mismatch`: a re-supplied chain entry whose path rules depend on the leg that installed it has
2081
+ * no installation record (a chain written before the record existed) / a record that does not cover the chain entry for
2082
+ * entry / a record member naming no absolute root. */
2083
+ | "installation_record_absent" | "installation_record_misaligned" | "installation_record_invalid";
2047
2084
  /**
2048
2085
  * The codes a RESUME's pre-claim configuration screen carries as `detail.code` of a `checkpoint.config_refused` — the seven
2049
2086
  * words prepare's mode, output-style, approval-reach, invoked-skill and recorded-mode doors throw, held unchanged so one refusal
@@ -20,9 +20,19 @@
20
20
  *
21
21
  * The fix is not a fourth arm in the lanes: it is that "which base does this tool resolve against, and what
22
22
  * does it target when the slot is empty" is a FACT OF THE TOOL, so the tool declares it once and both
23
- * sides read the declaration. {@link pathTargetBaseOf} is the shared SELECTION (which base), and each side
24
- * then applies its own identity to the selected base — lexical-normal here (the rule family's one path
25
- * identity, zero IO), canonical (symlink-resolving, IO) inside the tool.
23
+ * sides read the declaration. {@link pathTargetBaseOf} is the shared SELECTION (which base).
24
+ *
25
+ * ## The views of one target
26
+ *
27
+ * A target is seen three ways: its LEXICAL view (the spelling joined to the selected base, `.` / `..` folded —
28
+ * zero IO), its CANONICAL view (the same spelling resolved by the execution environment: symlinks, aliases of the
29
+ * tree, the disk's own case) and the HOPS of its leaf's symlink chain. {@link readPathViews} is the one producer of
30
+ * those views — the tool's own resolution and its deny judgment read it, and so does {@link readCallTarget}, which
31
+ * a Runner stamp point calls once per adjudicated call and hands to every rule lane as
32
+ * `ToolCallRequest.targetReading`. Every rule lane reads a call's target through ONE reader, {@link callTargetViewsOf}:
33
+ * the reading's views when the call carries one, else the call's own lexical view (a bare request). The judgment over
34
+ * those views is the deny tables' (`pathRowsReach`): any view a deny or ask row reaches is reached; an allow row admits a
35
+ * call only when it admits every view of the declared reading.
26
36
  *
27
37
  * ## What it does NOT decide
28
38
  *
@@ -35,7 +45,9 @@
35
45
  * rule family reaches it, and the rule grammar is derived from the catalog's path targets), so it has no
36
46
  * declaration to read; the day it declares one, it reads the same selector as everything else.
37
47
  */
38
- import { type PathFamily, type PathIdentity, type Win32NamespaceRefusal } from "../tools/fs/safety.js";
48
+ import { canonicalizeTarget, type PathFamily, type PathIdentity, type Win32NamespaceRefusal } from "../tools/fs/safety.js";
49
+ import type { TargetViews } from "../tools/fs/read-deny.js";
50
+ import type { ExecutionEnv } from "../internal/harness-types.js";
39
51
  import type { ToolCallPathTarget } from "./tool-face.js";
40
52
  import { type ToolCallLike } from "./tool-registry.js";
41
53
  /** The bases of ONE CALL a target resolves against: the task root, and the live tracked working directory
@@ -114,3 +126,123 @@ export declare function effectivePathSpellingOf(call: ToolCallLike, pt: ToolCall
114
126
  } | {
115
127
  readonly refused: Win32NamespaceRefusal;
116
128
  } | undefined;
129
+ /**
130
+ * ONE reading of a call's path slot as VIEWS. `path`: the LEXICAL view (the spelling joined to the selected base and
131
+ * folded — a native spelling of the tree's family), the CANONICAL view when the environment answered (absent ⇒ not asked
132
+ * or no answer: an absent view takes no part in a judgment and is never a refusal) and the HOPS of the leaf's symlink
133
+ * chain when the environment declares one. `unreadable` / `refused` read as in {@link PathTargetReading}.
134
+ */
135
+ export type CallTargetView = {
136
+ readonly kind: "path";
137
+ readonly lexical: string;
138
+ readonly canonical?: string;
139
+ readonly hops?: readonly string[];
140
+ } | {
141
+ readonly kind: "unreadable";
142
+ readonly reason: string;
143
+ } | {
144
+ readonly kind: "refused";
145
+ readonly reason: Win32NamespaceRefusal;
146
+ };
147
+ /**
148
+ * A call's TARGET READING: the views of each reading of its path slot — `protective`, the tightening direction's
149
+ * ({@link protectivePathTargetOf}), and `declared`, the allow direction's ({@link declaredPathTargetOf}; absent on a
150
+ * request that carries no face). It carries no root and no cwd: the call's own `root` / `cwd` are the only copies.
151
+ */
152
+ export interface CallTargetReading {
153
+ readonly protective?: CallTargetView;
154
+ readonly declared?: CallTargetView;
155
+ /** Where each location on the way to a rule row's head that this leg's lanes hold LEADS in the leg's environment (a
156
+ * link, a firmlink, an alias of the tree) — the rows' twins are judged there too. A fact of the judging leg, taken
157
+ * once per location per leg; absent when no row head leads elsewhere. */
158
+ readonly leads?: ReadonlyMap<string, string>;
159
+ }
160
+ /** The environment's answer the canonical view rests on — the throat's own, so the tool keeps reading the refusal it gave. */
161
+ export type CanonicalAnswer = Awaited<ReturnType<typeof canonicalizeTarget>>;
162
+ /** The three views of one spelling as {@link readPathViews} produces them. */
163
+ export interface PathViews {
164
+ readonly lexical: PathTargetReading;
165
+ /** `undefined` ⇒ the environment was not asked (none supplied, or a spelling this reading cannot locate). */
166
+ readonly canonical?: CanonicalAnswer;
167
+ readonly hops: readonly string[];
168
+ }
169
+ /**
170
+ * The ONE producer of a target's views: `spelled` against `base` (the base the tool's declaration selected), under the
171
+ * tree `bases` names. Lexical through the same resolution {@link effectivePathTargetOf} reads; canonical through the
172
+ * throat's own canonicalizer; the hops (when `opts.hops` and the canonical view answered) through the leaf's chain. The
173
+ * tool's resolution and deny judgment read this, and so does {@link readCallTarget}; an environment that THROWS
174
+ * propagates here (each caller holds its own posture: the tool refuses, the reading leaves the view absent).
175
+ */
176
+ export declare function readPathViews(env: ExecutionEnv | undefined, spelled: string, base: string | undefined, bases: ToolPathBases, signal?: AbortSignal, opts?: {
177
+ readonly hops?: boolean;
178
+ }): Promise<PathViews>;
179
+ /** The identity of an ABSOLUTE spelling under the tree `bases` names — the target side's one prelude (family, namespace
180
+ * screen, identity) for a path a judge already holds (a canonical key, a hop, a lexical view). `undefined` when the
181
+ * spelling is not an absolute path of that tree. */
182
+ export declare function absoluteTargetIdentityOf(spelled: string, bases: ToolPathBases): PathIdentity | undefined;
183
+ /**
184
+ * A call's target reading under ONE call's facts: its `root` / `cwd` (the bases the stamp point reads off the leg) and
185
+ * the execution environment `env` (absent ⇒ the lexical views alone — the pre-claim recheck, which has no environment).
186
+ * A canonical view the environment cannot answer (or throws on) is absent, never a refusal (an absent view does not vote).
187
+ * The two readings share their views when they select the same spelling against the same base (one canonicalization).
188
+ */
189
+ export declare function readCallTarget(env: ExecutionEnv | undefined, call: ToolCallLike & {
190
+ readonly root?: string | undefined;
191
+ readonly cwd?: string | undefined;
192
+ }, signal?: AbortSignal, leads?: ReadonlyMap<string, string | undefined>): Promise<CallTargetReading>;
193
+ /**
194
+ * `req` with its arguments replaced by `args` and — when `req` carries a reading of its own args — a reading of the NEW
195
+ * args taken the same way (same environment, same root / cwd, same face). Every seat that rewrites a request's arguments
196
+ * and hands it on (a composition's rewrite cascade, a re-judgment of the final or an edited form) goes through here, so the
197
+ * next layer never judges a request its reading does not describe. `signal` is the CURRENT call's: a reading never carries
198
+ * one call's cancellation into another's. `undefined` ⇒ nothing to re-read: use `{ ...req, args }`.
199
+ */
200
+ export declare function withArgsReading<R extends {
201
+ readonly args: unknown;
202
+ readonly targetReading?: CallTargetReading | undefined;
203
+ }>(req: R, args: unknown, signal: AbortSignal | undefined): Promise<R> | undefined;
204
+ /** The request's target reading, when it describes the request's own `args` (the object it was taken for); otherwise
205
+ * nothing — a rule lane then judges the lexical view it always judged. Every rule lane reads the reading through here. */
206
+ export declare function callTargetReadingOf(req: {
207
+ readonly args: unknown;
208
+ readonly targetReading?: CallTargetReading | undefined;
209
+ }): CallTargetReading | undefined;
210
+ /**
211
+ * THE one reader of a call's target for every rule lane, in one direction's reading (`tighten` = the protective reading,
212
+ * `allow` = the declared one): the views of the call's target reading when it carries one of its own args (lexical,
213
+ * canonical, the leaf chain's hops), else the call's own lexical view — resolved against the call's `root` / `cwd` when it
214
+ * carries either, else against `bare` (a host's direct call of a policy: the bases it declared). `undefined` = the call
215
+ * names no path in this reading (or a spelling the throat refuses — no rule speaks about it); an `unreadable` answer =
216
+ * the call names a path this reading cannot locate.
217
+ */
218
+ export declare function callTargetViewsOf(req: ToolCallLike & {
219
+ readonly root?: string | undefined;
220
+ readonly cwd?: string | undefined;
221
+ readonly targetReading?: CallTargetReading | undefined;
222
+ }, direction: "tighten" | "allow", bare?: ToolPathBases): TargetViews | undefined;
223
+ /**
224
+ * A leg's STAMP seat: `req` with a target reading of its own args, or `undefined` when there is nothing to stamp — the
225
+ * leg's lanes hold no path rule, the request already carries a reading of its args, or its args are not an object. The
226
+ * reading is taken under the request's own `root` / `cwd` when it carries them, else the leg's (the same values every
227
+ * Runner seat stamps), and memoized per args object within one call (its `toolCallId`), so every lane reading one call
228
+ * costs one reading. `undefined`
229
+ * rather than a resolved request, so a seat with nothing to do adds no suspension point.
230
+ */
231
+ export type CallTargetStamp = <R extends ToolCallLike & {
232
+ readonly toolCallId?: string;
233
+ readonly root?: string | undefined;
234
+ readonly cwd?: string | undefined;
235
+ readonly targetReading?: CallTargetReading | undefined;
236
+ }>(req: R, signal?: AbortSignal) => Promise<R & {
237
+ readonly targetReading: CallTargetReading;
238
+ }> | undefined;
239
+ /** The stamp seat of one leg: its execution environment, its root and live working directory, whether its lanes hold a
240
+ * path rule (read per call — an activated skill can add one), and where the heads of the rows its lanes hold lead in its
241
+ * environment (resolved once per location per leg). */
242
+ export declare function legCallTargetStamp(leg: {
243
+ readonly env: ExecutionEnv;
244
+ readonly root: string;
245
+ readonly cwd: () => string | undefined;
246
+ readonly judgesPaths: () => boolean;
247
+ readonly leads: () => Promise<ReadonlyMap<string, string | undefined>>;
248
+ }): CallTargetStamp;
@@ -1,5 +1,7 @@
1
- import { isAbsoluteForFamily, joinForFamily, pathFamilyOf, pathIdentityOf, win32NamespaceScreen } from "../tools/fs/safety.js";
2
- import { pathTargetValue } from "./tool-registry.js";
1
+ import { canonicalizeTarget, isAbsoluteForFamily, joinForFamily, pathFamilyOf, pathIdentityOf, spellingOfIdentity, win32NamespaceScreen } from "../tools/fs/safety.js";
2
+ import { declaredPathTargetOf, pathTargetValue, protectivePathTargetOf } from "./tool-registry.js";
3
+ import { symlinkChainHops } from "./path-identity.js";
4
+ import { DEFAULT_GATE_AWAIT_TIMEOUT_MS, withGateTimeout } from "./gate-bounded-await.js";
3
5
  export function pathTargetBaseOf(pt, bases) {
4
6
  return (pt?.base ?? "cwd") === "root" ? bases.root : (bases.cwd ?? bases.root);
5
7
  }
@@ -73,3 +75,109 @@ function readingOf(r) {
73
75
  function unreadableBase(base) {
74
76
  return { kind: "unreadable", reason: base === undefined ? "the call's path is relative to a base that was not supplied" : `the base this call resolves against (${JSON.stringify(base)}) is not a fully-qualified path of this tree's family` };
75
77
  }
78
+ export async function readPathViews(env, spelled, base, bases, signal, opts) {
79
+ const lexical = resolveSpelling(spelled, base, bases);
80
+ const family = pathFamilyOf(bases);
81
+ if (env === undefined || family === undefined || lexical?.kind === "unreadable")
82
+ return { lexical, hops: [] };
83
+ const canonical = await canonicalizeTarget(env, spelled, signal, family, base);
84
+ const climbs = (s) => s !== undefined && (family === "win" ? /(^|[\\/])\.\.([\\/]|$)/ : /(^|\/)\.\.(\/|$)/).test(s);
85
+ const followsALink = canonical.ok && (climbs(spelled) || climbs(base) || lexical?.kind !== "path" || absoluteTargetIdentityOf(canonical.key, bases) !== lexical.identity);
86
+ const hops = opts?.hops === true && followsALink ? await withGateTimeout(symlinkChainHops(env, spelled, signal, family, base), DEFAULT_GATE_AWAIT_TIMEOUT_MS, signal, "canonicalChain").catch(() => []) : [];
87
+ return { lexical, canonical, hops };
88
+ }
89
+ export function absoluteTargetIdentityOf(spelled, bases) {
90
+ const r = pathIdentityOf(spelled, pathFamilyOf(bases), bases.root ?? bases.cwd);
91
+ return r.ok ? r.identity : undefined;
92
+ }
93
+ const READ_FOR = new WeakMap();
94
+ const REREAD = new WeakMap();
95
+ export async function readCallTarget(env, call, signal, leads) {
96
+ const bases = { ...(call.root !== undefined ? { root: call.root } : {}), ...(call.cwd !== undefined ? { cwd: call.cwd } : {}) };
97
+ const family = pathFamilyOf(bases);
98
+ const views = new Map();
99
+ const viewOf = (pt) => {
100
+ const sel = effectivePathSpellingOf(call, pt, bases);
101
+ if (sel === undefined)
102
+ return Promise.resolve(undefined);
103
+ if ("refused" in sel)
104
+ return Promise.resolve({ kind: "refused", reason: sel.refused });
105
+ const target = sel.spelled !== undefined ? { spelled: sel.spelled, base: sel.base } : sel.base !== undefined && isAbsoluteForFamily(family, sel.base) ? { spelled: sel.base, base: undefined } : undefined;
106
+ if (target === undefined)
107
+ return Promise.resolve(unreadableBase(sel.base));
108
+ const { spelled, base } = target;
109
+ const key = JSON.stringify([spelled, base ?? null]);
110
+ const prior = views.get(key);
111
+ if (prior !== undefined)
112
+ return prior;
113
+ const read = readPathViews(env, spelled, base, bases, signal, { hops: true })
114
+ .catch(() => ({ lexical: resolveSpelling(spelled, base, bases), hops: [] }))
115
+ .then((v) => viewFrom(v, family));
116
+ views.set(key, read);
117
+ return read;
118
+ };
119
+ const protective = await viewOf(protectivePathTargetOf(call));
120
+ const declared = call.face === undefined ? undefined : await viewOf(declaredPathTargetOf(call));
121
+ const led = leads === undefined ? undefined : [...leads].filter((e) => e[1] !== undefined && e[1] !== e[0]);
122
+ const reading = Object.freeze({ ...(protective !== undefined ? { protective } : {}), ...(declared !== undefined ? { declared } : {}), ...(led !== undefined && led.length > 0 ? { leads: new Map(led) } : {}) });
123
+ if (typeof call.args === "object" && call.args !== null)
124
+ READ_FOR.set(reading, call.args);
125
+ REREAD.set(reading, (args, rereadSignal) => readCallTarget(env, { ...call, args }, rereadSignal, leads));
126
+ return reading;
127
+ }
128
+ export function withArgsReading(req, args, signal) {
129
+ const reading = callTargetReadingOf(req);
130
+ const reread = reading === undefined ? undefined : REREAD.get(reading);
131
+ return reread === undefined ? undefined : reread(args, signal).then((targetReading) => ({ ...req, args, targetReading }));
132
+ }
133
+ function viewFrom(v, family) {
134
+ if (v.lexical === undefined)
135
+ return undefined;
136
+ if (v.lexical.kind !== "path")
137
+ return Object.freeze({ ...v.lexical });
138
+ const canonical = v.canonical?.ok === true ? v.canonical.key : undefined;
139
+ return Object.freeze({ kind: "path", lexical: spellingOfIdentity(v.lexical.identity, family), ...(canonical !== undefined ? { canonical } : {}), ...(v.hops.length > 0 ? { hops: Object.freeze([...v.hops]) } : {}) });
140
+ }
141
+ export function callTargetReadingOf(req) {
142
+ const r = req.targetReading;
143
+ return r !== undefined && READ_FOR.get(r) === req.args ? r : undefined;
144
+ }
145
+ export function callTargetViewsOf(req, direction, bare = {}) {
146
+ const reading = callTargetReadingOf(req);
147
+ if (reading !== undefined) {
148
+ const v = direction === "tighten" ? reading.protective : reading.declared;
149
+ if (v === undefined || v.kind === "refused")
150
+ return undefined;
151
+ if (v.kind === "unreadable")
152
+ return { spellings: [], unreadable: v.reason };
153
+ const spellings = [v.lexical];
154
+ for (const s of [v.canonical, ...(v.hops ?? [])])
155
+ if (s !== undefined && !spellings.includes(s))
156
+ spellings.push(s);
157
+ return { spellings };
158
+ }
159
+ const own = req.root !== undefined || req.cwd !== undefined ? { ...(req.root !== undefined ? { root: req.root } : {}), ...(req.cwd !== undefined ? { cwd: req.cwd } : {}) } : bare;
160
+ const target = effectivePathTargetOf(req, direction === "tighten" ? protectivePathTargetOf(req) : declaredPathTargetOf(req), own);
161
+ if (target === undefined || target.kind === "refused")
162
+ return undefined;
163
+ if (target.kind === "unreadable")
164
+ return { spellings: [], unreadable: target.reason };
165
+ return { spellings: [spellingOfIdentity(target.identity, pathFamilyOf(own))] };
166
+ }
167
+ export function legCallTargetStamp(leg) {
168
+ const taken = new WeakMap();
169
+ return (req, signal) => {
170
+ const args = req.args;
171
+ if (typeof args !== "object" || args === null || callTargetReadingOf(req) !== undefined || !leg.judgesPaths())
172
+ return undefined;
173
+ const root = req.root ?? leg.root;
174
+ const cwd = req.cwd ?? leg.cwd();
175
+ const prior = taken.get(args);
176
+ const reading = prior !== undefined && prior.root === root && prior.cwd === cwd && prior.toolName === req.toolName && prior.toolCallId === req.toolCallId && prior.face === req.face
177
+ ? prior.reading
178
+ : leg.leads().then((leads) => readCallTarget(leg.env, { toolName: req.toolName, args, ...(req.face !== undefined ? { face: req.face } : {}), root, ...(cwd !== undefined ? { cwd } : {}) }, signal, leads));
179
+ if (prior?.reading !== reading)
180
+ taken.set(args, { root, cwd, toolName: req.toolName, toolCallId: req.toolCallId, face: req.face, reading });
181
+ return reading.then((targetReading) => ({ ...req, targetReading }));
182
+ };
183
+ }
@@ -368,6 +368,11 @@ export interface EngineNotice {
368
368
  * (one call's own rule check — every later call reads again, so the rows apply as soon as a read succeeds). Once
369
369
  * per run, whichever read fails first; the trace keeps every read's line (`permission.rule_store_unreadable`).
370
370
  * `detail: { at, sessionId, runId }`. Audience `user`.
371
+ * - `"permission.rule_row_dropped"` — the person's store served project-scoped rows whose scope root is not a
372
+ * complete absolute path (only a foreign writer or a hand edit makes one; every engine write face refuses it): they
373
+ * take no part in this run's checks — a deny or ask row among them protects nothing — until they are rewritten or
374
+ * removed. Once per run, by whichever read met them first. `detail: { rows, count, sessionId, runId }` — `rows`
375
+ * the first eight `{ rule, scopeRoot }` (each bounded), `count` all of them. Audience `user`.
371
376
  * - `"config.durable_gate_unavailable"` — a durable-approval MANDATE is in force on a leg with NO
372
377
  * checkpoint store: the per-principal `forceDurableGate` entitlement, or the task's own
373
378
  * `durableApproval` opt-in with no live approver seat (`durableMandateSourceOf`, the predicate the
@@ -829,11 +834,14 @@ export interface EngineNotice {
829
834
  * boot; deletes in it will fail until the operator restores the bit. Audience `"operator"`; `detail: { layers,
830
835
  * remedy }` — layer directory names and the one command. No session. Once per boot.
831
836
  * - `"server.model_unreachable"` — a model call of this leg ended in error and its last request failed to
832
- * connect. Audience `"user"`; `detail: { code, targetHost, remedy, sessionId, proxyInUse? }` — `code` one of
833
- * `UND_ERR_CONNECT_TIMEOUT` / `ECONNREFUSED` / `ENOTFOUND` / `EAI_AGAIN` / `EHOSTUNREACH` / `ENETUNREACH`;
837
+ * connect. Audience `"user"`; `detail: { code, targetHost, remedy, sessionId, taskId?, proxyInUse? }` — `code`
838
+ * one of `UND_ERR_CONNECT_TIMEOUT` / `ECONNREFUSED` / `ENOTFOUND` / `EAI_AGAIN` / `EHOSTUNREACH` / `ENETUNREACH`;
834
839
  * `targetHost` the model endpoint's `host:port`; `remedy` the constant `"check VPN / proxy; restart if the
835
- * proxy changed since boot"`; `proxyInUse` present only when that request went through a proxy, its value the
836
- * variable's name (`HTTP_PROXY` or `HTTPS_PROXY`), never the proxy address. No `runId`. Once per leg per target.
840
+ * proxy changed since boot"`; `taskId` (server 7.121.0+) the run resource id of the leg this notice belongs to —
841
+ * absent on a streaming leg that has no run store, and the SAME value on a resumed leg and its original (a
842
+ * per-leg dedup folds the two into one line); `proxyInUse` present only when that request went through a proxy,
843
+ * its value the variable's name (`HTTP_PROXY` or `HTTPS_PROXY`), never the proxy address. No `runId`. Once per
844
+ * leg per target.
837
845
  *
838
846
  * Deliberately NOT a notice family: brain retry/reconnect liveness (a rate limit, a 5xx, a
839
847
  * transient network failure being retried). Those are per-attempt liveness frames with their own
@@ -2,7 +2,7 @@ import { inlineUntrusted } from "./untrusted-text.js";
2
2
  import { markTightenMessage } from "./safety-axis-vocab.js";
3
3
  import { PROBE_REASON_MAX, normalizeProbeCause } from "./checkpoint-store.js";
4
4
  import { normalizeReadRootCandidate } from "./read-root-candidate.js";
5
- import {} from "./runner/gate-exit.js";
5
+ import { passAskFace } from "./runner/gate-exit.js";
6
6
  import { traceHookCrash } from "./gate-crash-trace.js";
7
7
  import { writeProtectionMandated, writeProtectionTightenDecision, writeProtectionUnresolved } from "./write-protect.js";
8
8
  import { createGateDeadline, awaitWriteProtectionJudgment, gateWaitFailureOf, withGateTimeout } from "./gate-bounded-await.js";
@@ -13,6 +13,9 @@ export async function runGateFold(pass) {
13
13
  const { input, toolName, toolCallId, callSignal, ledger, notifier, adjudicate } = pass;
14
14
  const boundedAwaitDeadlineMs = createGateDeadline(input, notifier);
15
15
  pass.req = { toolName, args: pass.currentInput, toolCallId, ...(pass.req.face !== undefined ? { face: pass.req.face } : {}) };
16
+ const stamped = input.stampCallTarget?.(pass.req, callSignal ?? input.abortSignal);
17
+ if (stamped !== undefined)
18
+ pass.req = await stamped;
16
19
  pass.decision = adjudicate ? refuseOutOfContractDecision(await (callSignal !== undefined ? adjudicate(pass.req, callSignal) : adjudicate(pass.req))) : { action: "allow" };
17
20
  {
18
21
  const refusedBy = ledger.fromDecision(pass.decision);
@@ -164,7 +167,7 @@ export async function runGateFold(pass) {
164
167
  pass.writeProtected = true;
165
168
  if (verdict !== undefined && writeProtectionUnresolved(verdict))
166
169
  pass.writeProtectionUnresolved = true;
167
- const decision = verdict === undefined ? undefined : writeProtectionTightenDecision(toolName, verdict);
170
+ const decision = verdict === undefined ? undefined : writeProtectionTightenDecision(toolName, verdict, passAskFace(pass));
168
171
  if (decision !== undefined && writeProtectionStanding === "allow") {
169
172
  pass.decision = decision;
170
173
  if (decision.action === "ask")