@sema-agent/core 7.49.5 → 7.50.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 (200) hide show
  1. package/CHANGELOG.md +157 -0
  2. package/dist/agents/observer.js +3 -10
  3. package/dist/agents/subagent-steps.d.ts +6 -14
  4. package/dist/agents/subagent-steps.js +3 -17
  5. package/dist/brain/stream-shared.d.ts +1 -1
  6. package/dist/core/allow-entry.d.ts +1 -1
  7. package/dist/core/allow-entry.js +1 -1
  8. package/dist/core/ask-origin.d.ts +15 -11
  9. package/dist/core/ask-origin.js +6 -14
  10. package/dist/core/auto-mode-prompt.js +6 -2
  11. package/dist/core/checkpoint-store.d.ts +6 -4
  12. package/dist/core/checkpoint-store.js +1 -1
  13. package/dist/core/context-edit.d.ts +2 -2
  14. package/dist/core/decision-receipt.d.ts +2 -2
  15. package/dist/core/decision-receipt.js +30 -11
  16. package/dist/core/delegation-frames.d.ts +66 -18
  17. package/dist/core/delegation-frames.js +22 -0
  18. package/dist/core/engine-notice.d.ts +7 -5
  19. package/dist/core/frame-settlement.d.ts +6 -1
  20. package/dist/core/gate-fold.js +34 -19
  21. package/dist/core/gate-lanes.js +15 -13
  22. package/dist/core/gate-outcome.d.ts +2 -2
  23. package/dist/core/governance-codes.js +2 -0
  24. package/dist/core/hooks.d.ts +28 -20
  25. package/dist/core/human-input-projection.js +2 -0
  26. package/dist/core/lsp-diagnostics.d.ts +3 -0
  27. package/dist/core/lsp-diagnostics.js +6 -0
  28. package/dist/core/lsp-session.d.ts +39 -17
  29. package/dist/core/lsp-session.js +118 -58
  30. package/dist/core/lsp.d.ts +83 -3
  31. package/dist/core/lsp.js +146 -16
  32. package/dist/core/mcp.js +2 -1
  33. package/dist/core/media-byte-cap.d.ts +14 -0
  34. package/dist/core/media-byte-cap.js +15 -3
  35. package/dist/core/memory-engine/consolidation-driver.d.ts +23 -1
  36. package/dist/core/memory-engine/consolidation-driver.js +54 -6
  37. package/dist/core/memory-engine/consolidation-write-gate.d.ts +46 -0
  38. package/dist/core/memory-engine/consolidation-write-gate.js +82 -0
  39. package/dist/core/memory-engine/consolidation.d.ts +17 -0
  40. package/dist/core/memory-engine/consolidation.js +1 -0
  41. package/dist/core/memory-engine/distiller.d.ts +8 -2
  42. package/dist/core/memory-engine/distiller.js +8 -6
  43. package/dist/core/memory-engine/engine.d.ts +37 -4
  44. package/dist/core/memory-engine/engine.js +127 -38
  45. package/dist/core/memory-engine/file-backend.d.ts +8 -3
  46. package/dist/core/memory-engine/file-backend.js +44 -16
  47. package/dist/core/memory-engine/index.d.ts +2 -2
  48. package/dist/core/memory-engine/index.js +1 -1
  49. package/dist/core/memory-engine/landing-admission.d.ts +65 -0
  50. package/dist/core/memory-engine/landing-admission.js +68 -0
  51. package/dist/core/memory-engine/types.d.ts +23 -0
  52. package/dist/core/mode-policy.d.ts +3 -2
  53. package/dist/core/mode-policy.js +2 -0
  54. package/dist/core/permission-rule-model.d.ts +19 -13
  55. package/dist/core/permission-rule-model.js +7 -4
  56. package/dist/core/permission-rule-provider.d.ts +11 -0
  57. package/dist/core/permission-rules.d.ts +1 -1
  58. package/dist/core/permission-rules.js +5 -2
  59. package/dist/core/persisted-rule-arms.d.ts +15 -7
  60. package/dist/core/persisted-rule-arms.js +13 -6
  61. package/dist/core/reminder-mint.d.ts +5 -0
  62. package/dist/core/reminder-mint.js +1 -0
  63. package/dist/core/retired-keys.d.ts +8 -0
  64. package/dist/core/retired-keys.js +4 -0
  65. package/dist/core/rule-lane-reads.d.ts +4 -4
  66. package/dist/core/rule-lane-reads.js +10 -1
  67. package/dist/core/runner/construction-doors.d.ts +10 -4
  68. package/dist/core/runner/construction-doors.js +25 -2
  69. package/dist/core/runner/contracts.d.ts +20 -3
  70. package/dist/core/runner/env-capability-census.js +1 -1
  71. package/dist/core/runner/gate-exit.js +2 -1
  72. package/dist/core/runner/memory-consolidation-driver.d.ts +6 -4
  73. package/dist/core/runner/memory-consolidation-driver.js +5 -2
  74. package/dist/core/runner/memory-consolidation-write-judge.d.ts +17 -0
  75. package/dist/core/runner/memory-consolidation-write-judge.js +14 -0
  76. package/dist/core/runner/permission-rule-lanes.d.ts +16 -22
  77. package/dist/core/runner/permission-rule-lanes.js +13 -43
  78. package/dist/core/runner/person-path-rows.d.ts +244 -0
  79. package/dist/core/runner/person-path-rows.js +387 -0
  80. package/dist/core/runner/policy-throw.d.ts +34 -0
  81. package/dist/core/runner/policy-throw.js +25 -0
  82. package/dist/core/runner/prepare-acquire-reconcile.js +8 -1
  83. package/dist/core/runner/prepare-artifact.d.ts +5 -3
  84. package/dist/core/runner/prepare-artifact.js +2 -8
  85. package/dist/core/runner/prepare-ask-lane.d.ts +2 -1
  86. package/dist/core/runner/prepare-ask-lane.js +14 -16
  87. package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -3
  88. package/dist/core/runner/prepare-caps-and-workflow.js +1 -1
  89. package/dist/core/runner/prepare-context-lane.js +2 -2
  90. package/dist/core/runner/prepare-gate-stations.d.ts +3 -3
  91. package/dist/core/runner/prepare-gate-stations.js +1 -1
  92. package/dist/core/runner/prepare-hands-readface.d.ts +19 -10
  93. package/dist/core/runner/prepare-hands-readface.js +57 -43
  94. package/dist/core/runner/prepare-lsp.d.ts +15 -13
  95. package/dist/core/runner/prepare-lsp.js +11 -11
  96. package/dist/core/runner/prepare-policy-chain.d.ts +8 -4
  97. package/dist/core/runner/prepare-policy-chain.js +11 -10
  98. package/dist/core/runner/prepare-run-refs.js +1 -1
  99. package/dist/core/runner/prepare-task.js +24 -16
  100. package/dist/core/runner/prepare-turn-wiring.d.ts +13 -6
  101. package/dist/core/runner/prepare-turn-wiring.js +19 -8
  102. package/dist/core/runner/read-root-grant.d.ts +1 -25
  103. package/dist/core/runner/read-root-grant.js +1 -17
  104. package/dist/core/runner/retired-runner-deps.d.ts +2 -2
  105. package/dist/core/runner/run-brain-sinks.d.ts +1 -5
  106. package/dist/core/runner/run-brain-sinks.js +2 -4
  107. package/dist/core/runner/run-harness-handlers.js +5 -4
  108. package/dist/core/runner/run-identity-wiring.d.ts +4 -4
  109. package/dist/core/runner/run-identity-wiring.js +10 -1
  110. package/dist/core/runner/run-leg.d.ts +1 -1
  111. package/dist/core/runner/run-leg.js +22 -9
  112. package/dist/core/runner/run-settle-and-teardown.d.ts +0 -4
  113. package/dist/core/runner/run-settle-and-teardown.js +7 -4
  114. package/dist/core/runner/run-terminal-adoption.d.ts +1 -1
  115. package/dist/core/runner/run-terminal-adoption.js +1 -1
  116. package/dist/core/runner/run-turn-boundary.js +2 -2
  117. package/dist/core/runner/runtask.js +8 -8
  118. package/dist/core/runner/session-file-state-replay.d.ts +14 -4
  119. package/dist/core/runner/session-file-state-replay.js +8 -0
  120. package/dist/core/runner/stream-steer-verb.js +3 -0
  121. package/dist/core/runner/tool-end-body.d.ts +5 -4
  122. package/dist/core/runner/tool-end-body.js +4 -4
  123. package/dist/core/runner/tool-output-projection.d.ts +6 -4
  124. package/dist/core/runner/tool-output-projection.js +66 -47
  125. package/dist/core/runner-deps.d.ts +6 -3
  126. package/dist/core/shared-memory/types.d.ts +2 -2
  127. package/dist/core/shell-read-silence.d.ts +23 -0
  128. package/dist/core/shell-read-silence.js +18 -0
  129. package/dist/core/synthetic-read-verdict.d.ts +80 -21
  130. package/dist/core/synthetic-read-verdict.js +89 -18
  131. package/dist/core/task-event.d.ts +113 -25
  132. package/dist/core/task-spec.d.ts +44 -0
  133. package/dist/core/task-tool-shape.d.ts +1 -1
  134. package/dist/core/tool-catalog-entries.js +16 -2
  135. package/dist/core/tool-policy.d.ts +25 -15
  136. package/dist/core/tool-policy.js +9 -13
  137. package/dist/core/trace.d.ts +3 -2
  138. package/dist/core/types.d.ts +3 -3
  139. package/dist/core/types.js +1 -1
  140. package/dist/core/untrusted-text.d.ts +2 -2
  141. package/dist/core/user-attachment.d.ts +164 -0
  142. package/dist/core/user-attachment.js +437 -0
  143. package/dist/engine/compaction/compaction.js +10 -3
  144. package/dist/engine/compaction/utils.js +6 -0
  145. package/dist/engine/execution-env/node-execution-env.d.ts +19 -1
  146. package/dist/engine/execution-env/node-execution-env.js +125 -0
  147. package/dist/engine/harness/agent-harness.d.ts +12 -2
  148. package/dist/engine/harness/agent-harness.js +15 -3
  149. package/dist/engine/harness/types.d.ts +21 -0
  150. package/dist/engine/llm/content-block-render.d.ts +15 -0
  151. package/dist/engine/llm/content-block-render.js +10 -0
  152. package/dist/engine/llm/index.d.ts +1 -0
  153. package/dist/engine/llm/index.js +1 -0
  154. package/dist/engine/llm/types.d.ts +80 -4
  155. package/dist/engine/llm/types.js +43 -1
  156. package/dist/engine/session/import-validate.js +2 -0
  157. package/dist/index.d.ts +6 -4
  158. package/dist/index.js +2 -1
  159. package/dist/internal/harness-types.d.ts +1 -1
  160. package/dist/internal/llm.d.ts +2 -2
  161. package/dist/internal/llm.js +1 -1
  162. package/dist/orchestration/workflow.d.ts +4 -5
  163. package/dist/orchestration/workflow.js +2 -2
  164. package/dist/prompts/default.d.ts +1 -1
  165. package/dist/tools/artifact/artifact-tool.js +1 -1
  166. package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -1
  167. package/dist/tools/fs/dir-entries.d.ts +4 -10
  168. package/dist/tools/fs/dir-entries.js +4 -11
  169. package/dist/tools/fs/file-read-check.js +3 -3
  170. package/dist/tools/fs/fs-pdf.js +2 -4
  171. package/dist/tools/fs/fs-read.d.ts +47 -4
  172. package/dist/tools/fs/fs-read.js +63 -19
  173. package/dist/tools/fs/fs-search-tools.d.ts +3 -3
  174. package/dist/tools/fs/fs-search-tools.js +7 -2
  175. package/dist/tools/fs/fs-write.d.ts +1 -1
  176. package/dist/tools/fs/fs-write.js +7 -6
  177. package/dist/tools/fs/index.d.ts +7 -1
  178. package/dist/tools/fs/index.js +56 -34
  179. package/dist/tools/fs/read-deny-home.d.ts +19 -1
  180. package/dist/tools/fs/read-deny-home.js +32 -22
  181. package/dist/tools/fs/read-deny.d.ts +14 -0
  182. package/dist/tools/fs/read-deny.js +8 -0
  183. package/dist/tools/fs/repo-map.d.ts +3 -2
  184. package/dist/tools/fs/repo-map.js +11 -3
  185. package/dist/tools/fs/safety.d.ts +41 -2
  186. package/dist/tools/fs/safety.js +12 -3
  187. package/dist/tools/fs/shell-read-check.d.ts +12 -3
  188. package/dist/tools/fs/shell-read-check.js +44 -24
  189. package/dist/tools/fs/shell-read-device.d.ts +1 -1
  190. package/dist/tools/fs/shell-read-face.d.ts +15 -0
  191. package/dist/tools/fs/shell-read-face.js +5 -0
  192. package/dist/tools/fs/shell-read-judge.d.ts +9 -4
  193. package/dist/tools/fs/shell-read-judge.js +1 -1
  194. package/dist/tools/loop-tick.d.ts +2 -2
  195. package/package.json +3 -1
  196. package/test/export-surface.snapshot.json +39 -1
  197. package/wire-changes/core@7.50.0.json +1699 -0
  198. package/wire-changes/core@7.50.1.json +65 -0
  199. package/dist/core/runner/persisted-path-rows.d.ts +0 -88
  200. package/dist/core/runner/persisted-path-rows.js +0 -108
@@ -52,9 +52,9 @@ import type { ContainmentFace } from "../tools/fs/containment-face.js";
52
52
  * their standing deny/ask rows (the tightening lexer's `unreadable` word: an expansion where a rule
53
53
  * reads a word, an unterminated quote, a syntax error — the answer's `reason`): whether one of those
54
54
  * rows forbids this call is unknown, so an allow tightens to an ask a person must clear and no automatic
55
- * lane may answer. A store that could not be READ is not this word: its rows take no part in the
56
- * decision (the call is judged as if the person had no persisted rows) and the run carries one
57
- * `permission.rule_store_unavailable` notice instead.
55
+ * lane may answer. A store (or a partition of it) that could not be READ is this word too — a row inside it
56
+ * may forbid the call and nobody can tell — with the class `governance_unavailable` instead of the releasable
57
+ * `rule_unreadable`, both bypass-immune; the run also carries one `permission.rule_store_unavailable` notice.
58
58
  * - `hook` — a deployment's PreToolUse screening face raised or co-raised the ask; the word is
59
59
  * engine-stamped at the fold and is the deployment's own per-call adjudication.
60
60
  * - `ask_rule` — an EXPLICIT `ask` permission rule matched: a person's standing "ask me each time",
@@ -103,9 +103,14 @@ export interface AskOriginFacts {
103
103
  readonly org: "rule" | "unavailable" | undefined;
104
104
  /** The gate's own record of its persisted-rule lane's READ: the store was read, but THIS CALL could not be read
105
105
  * against the person's deny/ask rows (the lane's answer carried the lexer's reason) — the origin
106
- * `rule_store_unavailable`. Absent: the lane read the call, or its store could not be read and its rows took no
107
- * part. Written only by the gate. */
106
+ * `rule_store_unavailable`, the class `rule_unreadable` (bypass-immune, like the store form's). Absent: the lane read the call, or the store
107
+ * could not be read (see {@link ruleStoreUnreadable}). Written only by the gate. */
108
108
  readonly ruleCallUnreadable?: boolean;
109
+ /** The gate's own record of the same read when a PARTITION of the store (or all of it) could not be read: a deny / ask
110
+ * row inside it may forbid this call and nobody can tell — the same origin `rule_store_unavailable`, the
111
+ * bypass-immune class `governance_unavailable` (a source of governance that cannot be read: only a person answers).
112
+ * Exclusive with {@link ruleCallUnreadable}. Written only by the gate. */
113
+ readonly ruleStoreUnreadable?: boolean;
109
114
  /** Which of the gate's OWN post-fold stations minted the standing ask, if one did — again the gate's
110
115
  * record of its own layer (a station replaces an `allow` wholesale, so at most one fires per pass),
111
116
  * never a read-back of `decisionReason: "safety"` off a decision a policy could have composed. `unstated` is
@@ -569,7 +574,7 @@ export declare const MODE_ARBITRATION: {
569
574
  readonly hookAskFloor: false;
570
575
  };
571
576
  readonly rule_unreadable: {
572
- readonly bypassImmune: false;
577
+ readonly bypassImmune: true;
573
578
  readonly classifierEligible: false;
574
579
  readonly sandboxAdmission: false;
575
580
  readonly ruleMayClear: false;
@@ -713,14 +718,13 @@ export declare const RRA_EXPLAINERS: ReadonlySet<AskClass>;
713
718
  export declare function releasableAskClassesFor(face: AskFace): readonly AskClass[];
714
719
  /**
715
720
  * The closed set of `reason` words a mode release carries (`GateDisposition.released.reason`, the `released` arm of
716
- * {@link Arbitration}): each is a releasable class the release record names on its own — `rule_unreadable`, a release
717
- * over a person's rule the call could not be read against; `target_unresolved`, a release (the shell face only,
718
- * {@link RELEASED_ON_SHELL_FACE}) over a write target whose reading could not say where it leads. A release carries the first member (array order) its class
719
- * set holds, and none when it holds no member. {@link arbitrateByMode} and {@link isModeReleaseReason} read one copy
721
+ * {@link Arbitration}): each is a releasable class the release record names on its own — `target_unresolved`, a release
722
+ * (the shell face only, {@link RELEASED_ON_SHELL_FACE}) over a write target whose reading could not say where it leads. A
723
+ * release carries the first member (array order) its class set holds, and none when it holds no member. {@link arbitrateByMode} and {@link isModeReleaseReason} read one copy
720
724
  * of this list taken at load (a caller that writes into the exported array changes neither), so a new reason is one
721
725
  * word here.
722
726
  */
723
- export declare const MODE_RELEASE_REASONS: readonly ["rule_unreadable", "target_unresolved"];
727
+ export declare const MODE_RELEASE_REASONS: readonly ["target_unresolved"];
724
728
  export type ModeReleaseReason = (typeof MODE_RELEASE_REASONS)[number];
725
729
  /** Membership test for {@link ModeReleaseReason} — the word crosses the wire on `tool_end.gate.disposition.released.reason`. */
726
730
  export declare function isModeReleaseReason(v: unknown): v is ModeReleaseReason;
@@ -77,7 +77,7 @@ function originLadder(decision, facts) {
77
77
  return "org_unavailable";
78
78
  if (facts.org === "rule")
79
79
  return "org_rule";
80
- if (facts.ruleCallUnreadable === true)
80
+ if (facts.ruleCallUnreadable === true || facts.ruleStoreUnreadable === true)
81
81
  return "rule_store_unavailable";
82
82
  if (decision.decisionReason === "hook")
83
83
  return "hook";
@@ -210,7 +210,7 @@ export const MODE_ARBITRATION = {
210
210
  transcript_write: { bypassImmune: false, classifierEligible: true, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: true },
211
211
  org_rule: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false },
212
212
  governance_unavailable: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false },
213
- rule_unreadable: { bypassImmune: false, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false },
213
+ rule_unreadable: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false },
214
214
  ask_rule: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: true },
215
215
  denial_limit_fallback: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false },
216
216
  classifier_no_ruling: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false },
@@ -247,7 +247,7 @@ export function releasableAskClassesFor(face) {
247
247
  throw new TypeError(`releasableAskClassesFor: "${String(face)}" is not an ask face (${ASK_FACES.join(", ")})`);
248
248
  return RELEASABLE_BY_FACE[face];
249
249
  }
250
- export const MODE_RELEASE_REASONS = Object.freeze(["rule_unreadable", "target_unresolved"]);
250
+ export const MODE_RELEASE_REASONS = Object.freeze(["target_unresolved"]);
251
251
  const MODE_RELEASE_REASON_SET = new Set(MODE_RELEASE_REASONS);
252
252
  export function isModeReleaseReason(v) {
253
253
  return MODE_RELEASE_REASON_SET.has(v);
@@ -266,16 +266,8 @@ export function screenAskClassMembers(askClass, classes) {
266
266
  }
267
267
  export function decisionAskClassesOf(d) {
268
268
  const out = new Set();
269
- if (d.guard === "dangerous_delete")
270
- out.add("dangerous_delete");
271
- if (d.guard === "unverifiable_delete")
272
- out.add("unverifiable_delete");
273
- if (d.guard === "unverifiable_shell_text")
274
- out.add("unverifiable_shell_text");
275
- if (d.guard === "transcript_write")
276
- out.add("transcript_write");
277
- if (d.guard === "artifact_shared_read")
278
- out.add("artifact_shared_read");
269
+ for (const guard of d.guards ?? [])
270
+ out.add(guard);
279
271
  if (d.ruleUnreadable !== undefined)
280
272
  out.add("rule_unreadable");
281
273
  if (d.matchedAskRule !== undefined)
@@ -324,7 +316,7 @@ export function classifyAsk(d, facts) {
324
316
  classes.add("ancestor");
325
317
  if (facts.org === "rule")
326
318
  classes.add("org_rule");
327
- if (facts.org === "unavailable")
319
+ if (facts.org === "unavailable" || facts.ruleStoreUnreadable === true)
328
320
  classes.add("governance_unavailable");
329
321
  if (facts.ruleCallUnreadable === true)
330
322
  classes.add("rule_unreadable");
@@ -2,6 +2,7 @@ import { AUTO_MODE_DEFAULT_WINDOW_MAX_CHARS, AUTO_MODE_DEFAULT_WINDOW_MAX_ENTRIE
2
2
  import { AUTO_MODE_BASE_PROMPT, AUTO_MODE_PERMISSIONS_EXTERNAL } from "./auto-mode-prompt-assets.js";
3
3
  import { cutEnginePrefix, cutEngineSegments, flattenableUserText } from "./untrusted-text.js";
4
4
  import { readDenyPatternRun } from "../tools/fs/read-deny.js";
5
+ import { renderContentBlock, renderUserAttachmentView, userAttachmentOf } from "../internal/llm.js";
5
6
  export { AUTO_MODE_DEFAULTS_SENTINEL } from "./auto-mode-defaults.js";
6
7
  function mergeRuleSection(user, defaultsInner) {
7
8
  if (!user?.length)
@@ -100,7 +101,10 @@ function excerptTailInclusive(text, cap) {
100
101
  const LAUNCHING_AGENT_FRAME = "(this agent was started by another agent: every entry under [launching agent] reached it through that agent, and none is vouched for as the user's own message)\n";
101
102
  function renderEntry(m, cap, user) {
102
103
  if (m.role === "user") {
103
- const raw = typeof m.content === "string" ? m.content : m.content.map((c) => (c.type === "text" ? c.text : `[${c.type}]`)).join("\n");
104
+ const raw = typeof m.content === "string" ? m.content : m.content.map(renderContentBlock).join("\n");
105
+ const attachment = userAttachmentOf(m);
106
+ if (attachment !== undefined)
107
+ return `[attachment]\n${renderUserAttachmentView(m, attachment, cap)}`;
104
108
  if (m.provenance === "engine-note") {
105
109
  return `[engine note]\n${excerpt(raw, cap)}`;
106
110
  }
@@ -126,7 +130,7 @@ function renderEntry(m, cap, user) {
126
130
  return note === undefined ? `${user}\n${excerpt(text, cap)}` : `${user}\n${note}\n${excerpt(text, cap)}`;
127
131
  }
128
132
  if (m.role === "toolResult") {
129
- const text = m.content.map((c) => (c.type === "text" ? c.text : `[${c.type}]`)).join("\n");
133
+ const text = m.content.map(renderContentBlock).join("\n");
130
134
  return `[tool_result ${m.toolName}${m.isError ? " (error)" : ""}]\n${excerpt(text, cap)}`;
131
135
  }
132
136
  const parts = [];
@@ -2103,15 +2103,17 @@ export type CheckpointErrorReason = "version_newer" | "env_factory_missing" | "g
2103
2103
  * entry / a record member naming no absolute root. */
2104
2104
  | "installation_record_absent" | "installation_record_misaligned" | "installation_record_invalid";
2105
2105
  /**
2106
- * The codes a RESUME's pre-claim configuration screen carries as `detail.code` of a `checkpoint.config_refused` — the seven
2107
- * words prepare's mode, output-style, approval-reach, invoked-skill and recorded-mode doors throw, held unchanged so one refusal
2106
+ * The codes a RESUME's pre-claim configuration screen carries as `detail.code` of a `checkpoint.config_refused` — the eight
2107
+ * words prepare's mode, output-style, approval-reach, invoked-skill, attachment and recorded-mode doors throw, held unchanged so one refusal
2108
2108
  * reads the same on a fresh run (`TaskResult.errorCode`) and on a resume. `config.spec_retired_key`: a retired spec key;
2109
2109
  * `config.output_style_invalid`: a malformed output-style seat; `config.permission_mode_invalid`: a mode word outside the closed
2110
2110
  * set (the config's `permissionMode`, or one the row recorded); `config.shell_gate_value_retired`: a retired shell-floor word;
2111
2111
  * `config.approval_reach_invalid`: an `approvalReach` outside its set; `config.user_invoked_skill_unknown` / `skill_deny_unreadable`:
2112
- * `userInvokedSkills` names a skill the config does not carry / one whose `disallowed-tools` entry the grammar reads as nothing.
2112
+ * `userInvokedSkills` names a skill the config does not carry / one whose `disallowed-tools` entry the grammar reads as nothing;
2113
+ * `config.user_attachment_invalid`: the config declares `userAttachments` (a resume sends no new human message) or a malformed
2114
+ * value there.
2113
2115
  */
2114
- export declare const CHECKPOINT_CONFIG_REFUSAL_CODES: readonly ["config.spec_retired_key", "config.output_style_invalid", "config.permission_mode_invalid", "config.shell_gate_value_retired", "config.approval_reach_invalid", "config.user_invoked_skill_unknown", "config.skill_deny_unreadable"];
2116
+ export declare const CHECKPOINT_CONFIG_REFUSAL_CODES: readonly ["config.spec_retired_key", "config.output_style_invalid", "config.permission_mode_invalid", "config.shell_gate_value_retired", "config.approval_reach_invalid", "config.user_invoked_skill_unknown", "config.skill_deny_unreadable", "config.user_attachment_invalid"];
2115
2117
  export type CheckpointConfigRefusalCode = (typeof CHECKPOINT_CONFIG_REFUSAL_CODES)[number];
2116
2118
  export declare const isCheckpointConfigRefusalCode: (word: unknown) => word is CheckpointConfigRefusalCode;
2117
2119
  /** A typed checkpoint-store error so callers branch on `code` (mirrors `SessionError`). */
@@ -304,7 +304,7 @@ export function summarizeCheckpoint(cp) {
304
304
  ...(hasBidi ? { hasBidiControls: true } : {}),
305
305
  };
306
306
  }
307
- export const CHECKPOINT_CONFIG_REFUSAL_CODES = Object.freeze(["config.spec_retired_key", "config.output_style_invalid", "config.permission_mode_invalid", "config.shell_gate_value_retired", "config.approval_reach_invalid", "config.user_invoked_skill_unknown", "config.skill_deny_unreadable"]);
307
+ export const CHECKPOINT_CONFIG_REFUSAL_CODES = Object.freeze(["config.spec_retired_key", "config.output_style_invalid", "config.permission_mode_invalid", "config.shell_gate_value_retired", "config.approval_reach_invalid", "config.user_invoked_skill_unknown", "config.skill_deny_unreadable", "config.user_attachment_invalid"]);
308
308
  export const isCheckpointConfigRefusalCode = (word) => CHECKPOINT_CONFIG_REFUSAL_CODES.some((w) => w === word);
309
309
  export class CheckpointError extends Error {
310
310
  code;
@@ -77,7 +77,7 @@ export declare function contextEditFrontier(window: number): number;
77
77
  * runner wires it on BOTH arms by default — the frontier pass and the MC-R rejection arm share
78
78
  * one persist closure — so the shipped default clears to a pointer, never to a dead end.
79
79
  * ④ the TRIGGER — CC's client machine fires ONLY on the server's `context_hint` 422/424 refusal
80
- * (cli250.js:58375; `tengu_time_based_microcompact` is a historical event name, its `trigger`
80
+ * (cli250.js:58375 `context_hint`; the `tengu_time_based_microcompact` event name is historical, its `trigger`
81
81
  * is the constant `"context_hint"`). Under BYOM there is no such server, so the ask half is
82
82
  * unportable and the refusal half is aligned on wire truth (the MC-R arm classifies the
83
83
  * provider's own context-overflow refusal) — registered divergence D-3. sema additionally
@@ -92,7 +92,7 @@ export declare const MICRO_COMPACT_MACHINES: readonly ["off", "cc"];
92
92
  export type MicroCompactMachine = (typeof MICRO_COMPACT_MACHINES)[number];
93
93
  /**
94
94
  * How many most-recent CANDIDATE tool results keep their content when the stale-result pass runs:
95
- * 5 (CC 2.1.223 `uAp`; 2.1.250 `var k = 5` at the call site cli250.js:58375, and 2.1.245 carries
95
+ * 5 (CC 2.1.223 `uAp`; 2.1.250 `var k=5` beside the call site, cli250.js:58373, and 2.1.245 carries
96
96
  * the same value — zero drift across 245→250; same value again on CC 88's `timeBasedMCConfig`, i.e.
97
97
  * stable across all three corpus generations, and it is a hard-coded constant upstream, not a
98
98
  * setting). The #384 adjudication adopted 5 CONDITIONAL on the whole machine coming with it (20k
@@ -40,7 +40,7 @@ export declare function nestedRecordsOf(receipt: object): object[];
40
40
  * carrier, the member named by `path`: an accessor (never invoked), and a member the prototype or a non-enumerable property
41
41
  * supplies, unless the nearest descriptor holding it spells absence (an `undefined` data property). Descriptors are looked up
42
42
  * as OWN entries of the descriptor map, so a polluted `Object.prototype` names no member. Members outside the contract are
43
- * not looked at. */
43
+ * not looked at — but a retired one (`retired`) is refused by name when the carrier carries it by that same law. */
44
44
  /** The receipt's own answers carry a `kind` discriminant as an own property: narrowing never asks the prototype chain (`in`) for a
45
45
  * word, so a polluted `Object.prototype` cannot answer for the receipt either. */
46
46
  export type Received = {
@@ -50,5 +50,5 @@ export type Received = {
50
50
  kind: "refused";
51
51
  refused: PermissionResult;
52
52
  };
53
- export declare function receiveContract(carrier: object, members: readonly string[], path: string): Received;
53
+ export declare function receiveContract(carrier: object, members: readonly string[], path: string, retired?: Readonly<Record<string, string>>): Received;
54
54
  export {};
@@ -12,7 +12,7 @@ export const isEngineRefusal = (d) => typeof d === "object" && d !== null && ENG
12
12
  const DECISION_REASONS = Object.freeze(["rule", "mode", "hook", "safety", "classifier", "persisted_rule", "sandbox", "org_rule", "org_unavailable", "read_only", "skill", "deployment_rule", "request_rule"]);
13
13
  const DECISION_ACTIONS = Object.keys({ allow: 0, deny: 0, ask: 0 });
14
14
  const GUARDS = Object.keys({ dangerous_delete: 0, unverifiable_delete: 0, unverifiable_shell_text: 0, transcript_write: 0, artifact_shared_read: 0 });
15
- const RULE_UNREADABLE_WORDS = Object.keys({ content_deny: 0, content_ask: 0, persisted_call: 0 });
15
+ const RULE_UNREADABLE_WORDS = Object.keys({ content_deny: 0, content_ask: 0, persisted_call: 0, persisted_rows: 0 });
16
16
  const AUTHORED_BY_WORDS = Object.keys({ deployment: 0 });
17
17
  const memberOf = (words) => {
18
18
  const set = new Set(words);
@@ -26,7 +26,7 @@ const CLOSED_MEMBERS = [
26
26
  { member: "classes", words: ASK_CLASSES, has: isAskClass, list: true },
27
27
  { member: "foldedAskClasses", words: ASK_CLASSES, has: isAskClass, list: true },
28
28
  { member: "mandate", words: PERSISTED_RULE_MANDATES, has: isPersistedRuleMandate },
29
- { member: "guard", words: GUARDS, has: memberOf(GUARDS) },
29
+ { member: "guards", words: GUARDS, has: memberOf(GUARDS), list: true },
30
30
  { member: "ruleUnreadable", words: RULE_UNREADABLE_WORDS, has: memberOf(RULE_UNREADABLE_WORDS) },
31
31
  { member: "authoredBy", words: AUTHORED_BY_WORDS, has: memberOf(AUTHORED_BY_WORDS) },
32
32
  { member: "classifierNoRuling", words: AUTO_MODE_FALLBACK_CAUSES, has: isAutoModeFallbackCause },
@@ -197,7 +197,7 @@ function ordinaryChain(carrier) {
197
197
  }
198
198
  return { kind: "chain", chain };
199
199
  }
200
- export function receiveContract(carrier, members, path) {
200
+ export function receiveContract(carrier, members, path, retired) {
201
201
  const what = path === "" ? "a permission decision" : `a permission decision's "${path.slice(0, -1)}" record`;
202
202
  const ordinary = ordinaryChain(carrier);
203
203
  if (ordinary.kind === "not") {
@@ -210,6 +210,21 @@ export function receiveContract(carrier, members, path) {
210
210
  catch (e) {
211
211
  return { kind: "refused", refused: refusedOutOfContract(`${what} threw while its property descriptors were looked at (${thrownText(e)}) — not an ordinary object; denied fail-closed`) };
212
212
  }
213
+ const nearest = (m) => {
214
+ try {
215
+ if (Object.hasOwn(own, m))
216
+ return { kind: "found", d: own[m] };
217
+ for (let i = 1; i < ordinary.chain.length; i++) {
218
+ const d = Object.getOwnPropertyDescriptor(ordinary.chain[i], m);
219
+ if (d !== undefined)
220
+ return { kind: "found", d };
221
+ }
222
+ return { kind: "found", d: undefined };
223
+ }
224
+ catch (e) {
225
+ return { kind: "refused", refused: refusedOutOfContract(`${what} threw while "${path}${m}" was looked for on its prototype chain (${thrownText(e)}) — not an ordinary object; denied fail-closed`) };
226
+ }
227
+ };
213
228
  const held = Object.create(null);
214
229
  for (const m of members) {
215
230
  const d = Object.hasOwn(own, m) ? own[m] : undefined;
@@ -220,14 +235,10 @@ export function receiveContract(carrier, members, path) {
220
235
  held[m] = d.value;
221
236
  continue;
222
237
  }
223
- let reached = d;
224
- try {
225
- for (let i = 1; reached === undefined && i < ordinary.chain.length; i++)
226
- reached = Object.getOwnPropertyDescriptor(ordinary.chain[i], m);
227
- }
228
- catch (e) {
229
- return { kind: "refused", refused: refusedOutOfContract(`${what} threw while "${path}${m}" was looked for on its prototype chain (${thrownText(e)}) — not an ordinary object; denied fail-closed`) };
230
- }
238
+ const found = nearest(m);
239
+ if (found.kind === "refused")
240
+ return found;
241
+ const reached = found.d;
231
242
  if (reached !== undefined && !spellsAbsence(reached)) {
232
243
  return {
233
244
  kind: "refused",
@@ -236,5 +247,13 @@ export function receiveContract(carrier, members, path) {
236
247
  };
237
248
  }
238
249
  }
250
+ for (const [m, replacement] of Object.entries(retired ?? {})) {
251
+ const found = nearest(m);
252
+ if (found.kind === "refused")
253
+ return found;
254
+ if (found.d !== undefined && !spellsAbsence(found.d)) {
255
+ return { kind: "refused", refused: refusedOutOfContract(`a permission decision carries the retired "${m}" member — ${replacement}; denied fail-closed rather than executing a decision with a member this layer does not read`) };
256
+ }
257
+ }
239
258
  return { kind: "held", held };
240
259
  }
@@ -7,11 +7,18 @@
7
7
  */
8
8
  import type { TaskStatus } from "./task-result.js";
9
9
  import type { RunnerDeps } from "./runner-deps.js";
10
+ import type { TaskEvent } from "./task-event.js";
10
11
  /**
11
12
  * design/129-B — one lifecycle event of a BACKGROUND delegation child, delivered to the
12
13
  * process-level {@link RunnerDeps.onBackgroundChildEvent} observer. Keys: `taskId` is the registry handle
13
14
  * (`a*` domain — the TaskOutput/TaskStop key); `progressTaskId`/`sessionId` are the child RUN's id (uuid
14
15
  * domain — the same key `task_progress` frames carry).
16
+ *
17
+ * `kind: "spawn"` announces a fleet ROW's birth, which is a different fact from a child RUN's start
18
+ * (`TaskEvent` `task_started`): an `a*` row is born when its lane registers it, before the child run
19
+ * prepares (and a child whose prepare throws has a row and no run start); a `wa*` row is born with the
20
+ * workflow's own agent record, including rows that never run a child at all (a replayed or re-parked
21
+ * agent). Each fact has one mint — the lane for the row, the child's spawn station for the run.
15
22
  */
16
23
  export interface BackgroundChildEvent {
17
24
  kind: "spawn" | "tick" | "terminal";
@@ -223,15 +230,19 @@ export interface BackgroundChildEvent {
223
230
  * now sees every delegation lane through one seat.
224
231
  *
225
232
  * EMISSION CHOKEPOINT (the reason this seat covers every lane at once): frames are minted by the
226
- * RUNNER at the child LEG itself — spawn right after the leg's `wiring_manifest` (prepared, nothing
227
- * run yet), terminal when the leg's `TaskResult` assembles — not by the individual spawn lanes. Every
228
- * delegation lane (sync/steer/background/fork/revive, workflow-spawned agents) runs its child through
229
- * this chokepoint, so none of them needs its own emission and none can drift.
233
+ * RUNNER at the child LEG itself — not by the individual spawn lanes. Every delegation lane
234
+ * (sync/steer/background/fork/revive, workflow-spawned agents) runs its child through this
235
+ * chokepoint, so none of them needs its own emission and none can drift.
230
236
  *
231
- * PER-LEG semantics, deliberately: a durable park + resume, or a retained child's revive, is a NEW
232
- * leg — each emits its own spawn/terminal pair, and `identity.legKind` (`"resume"`) says which cycle
233
- * a frame belongs to. Consumers correlate legs of one delegation by `identity.taskId` /
234
- * `identity.parentToolCallId` (stable across cycles).
237
+ * SPAWN IS A PROJECTION, TERMINAL IS PER LEG. The `spawn` phase is not a fact of its own: it is the
238
+ * projection of the child's `task_started` stream frame (`TaskEvent`), delivered from that frame's one
239
+ * mint at the leg's spawn station, at the same instant and under the same gate — so it fires exactly
240
+ * when a CYCLE starts (a first spawn, or a revival of a completed child) and never for a durable
241
+ * RESUME leg, which continues a cycle that already started. The `terminal` phase fires for EVERY
242
+ * delegated leg, resume legs included (a park ends a leg with `"suspended"`; the resume leg that later
243
+ * redeems it ends with its own terminal). Consumers correlate legs of one delegation by
244
+ * `identity.taskId` / `identity.parentToolCallId` (stable across legs), and `identity.legKind`
245
+ * (`"resume"`) names a resume leg's terminal.
235
246
  *
236
247
  * HONEST ABSENCES (recorded, not gaps to fix silently):
237
248
  * · a child whose PREPARE throws emits neither frame (there is no leg identity to report);
@@ -242,14 +253,15 @@ export interface BackgroundChildEvent {
242
253
  * ({@link import("./checkpoint-store.js").CheckpointState.isDelegatedChild}) emits neither frame:
243
254
  * the row carries no evidence the parked leg was a delegated child, and fabricating the axis
244
255
  * would stamp delegation frames onto host-resumed root tasks. Rows minted WITH the axis resume
245
- * with their spawn/terminal pair even deps-only; a resume that re-supplies trusted internals
246
- * (every in-engine lane does) was never affected.
247
- * PAIRING (closed the one hole): a spawn frame is always closed by a terminal frame —
248
- * the ordinary path emits it where the `TaskResult` assembles, and a POST-SPAWN throw that the
249
- * stream layer converts into a synthesized failed result (e.g. `resume.tool_unavailable` re-thrown
250
- * past the run tail) emits the failed terminal from that backstop, gated on a carrier that is set
251
- * only after the spawn emission and cleared by the ordinary terminal (so the two sites can never
252
- * both fire for one leg). A leg whose PROCESS dies mid-flight is the only unpaired spawn.
256
+ * with their terminal even deps-only; a resume that re-supplies trusted internals (every
257
+ * in-engine lane does) was never affected.
258
+ * TERMINAL OWED (closed the one hole): every delegated leg that reached its wiring is closed by
259
+ * exactly one terminal frame — the ordinary path emits it where the `TaskResult` assembles, and a
260
+ * POST-WIRING throw that the stream layer converts into a synthesized failed result (e.g.
261
+ * `resume.tool_unavailable` re-thrown past the run tail) emits the failed terminal from that
262
+ * backstop, gated on a carrier set at the spawn station and cleared by the ordinary terminal (so the
263
+ * two sites can never both fire for one leg). A leg whose PROCESS dies mid-flight is the only one
264
+ * left without its terminal.
253
265
  *
254
266
  * OBSERVATION ONLY (matrix §5.2 Q3: frame-rate lifecycle facts ride an observer seat, never
255
267
  * onNotice): no return capability, and delivery can never alter the child run —
@@ -265,23 +277,59 @@ export interface BackgroundChildEvent {
265
277
  * `frame.hasOwnProperty(...)` (throws) or implicit string coercion (`` `${frame}` `` throws).
266
278
  */
267
279
  export type DelegationLifecycleEvent = {
280
+ /** A cycle started — the projection of the leg's `task_started` frame (one per first spawn or
281
+ * revival; never for a resume leg, so `identity.legKind` is `"child"` on every spawn frame). */
268
282
  phase: "spawn";
269
283
  /** The child LEG's identity envelope (#281 件A — the same frozen object that leg's own hook
270
284
  * invocations carry). `isDelegatedChild` is `true` by construction on every frame here. */
271
285
  identity: import("./hooks.js").HookInvocationIdentity;
272
286
  } | {
273
287
  phase: "terminal";
274
- /** Same envelope as the leg's spawn frame (one mint per leg). */
288
+ /** The leg's identity envelope (one mint per leg — the same object as the cycle's spawn frame on
289
+ * the leg that started it). */
275
290
  identity: import("./hooks.js").HookInvocationIdentity;
276
291
  /** The leg's settled status word — the projection of the `TaskResult.terminal` the spawning lane
277
292
  * receives (the sync lane's previously-void `settled` payload, made public). `"suspended"` means a
278
- * durable park: expect a later `"resume"`-leg spawn/terminal pair if it is redeemed. */
293
+ * durable park: expect a later `"resume"`-leg terminal if it is redeemed (a resume leg starts no
294
+ * cycle, so no spawn frame precedes it). */
279
295
  status: TaskStatus;
280
296
  /** Turns the leg completed. */
281
297
  turns: number;
282
298
  /** The leg's terminal code (the failed cause's `code`), when one was stamped. */
283
299
  errorCode?: string;
284
300
  };
301
+ /**
302
+ * The ONE eligibility reading for a leg's delegated-child ROW frames — its `task_started`, its
303
+ * `task_progress` ticks and its terminal tick all read this and nothing else: a delegated leg, by the
304
+ * identity envelope's effective delegation fact. A row a consumer can open is a row it can close, so
305
+ * no frame of the family may ask a narrower question (whether a spawning tool call is known is
306
+ * attribution, carried on the frames when it exists — never eligibility).
307
+ */
308
+ export declare function emitsChildRowFrames(identity: import("./hooks.js").HookInvocationIdentity): boolean;
309
+ /**
310
+ * The ONE constructor of a delegated child's `task_started` frame ({@link TaskEvent}); the leg's spawn
311
+ * station calls it once per started cycle. Every member is copied from the seat its `task_progress`
312
+ * twin (or the fleet row) reads — the run ids, the prepared model, the delegation facts the spawner
313
+ * threaded, and the display label as the identity envelope bounds it, present under the tick's own
314
+ * law (a label that bounds to nothing is no label) — so the frame introduces no second reading of any
315
+ * of them. `stamp` is the event identity the leg stamps on every frame.
316
+ */
317
+ export declare function taskStartedFrame(run: {
318
+ taskId: string;
319
+ sessionId: string;
320
+ model: string;
321
+ identity: import("./hooks.js").HookInvocationIdentity;
322
+ }, spawner: {
323
+ delegationTaskType?: import("./task-event.js").DelegationTaskType;
324
+ cycleSeq?: number;
325
+ parentTaskId?: string;
326
+ parentSessionId?: string;
327
+ rootSessionId?: string;
328
+ explicitAgentName?: string;
329
+ spawnAgentType?: string;
330
+ } | undefined, stamp: import("./task-event.js").TaskEventIdentity): Extract<TaskEvent, {
331
+ type: "task_started";
332
+ }>;
285
333
  /** Test seam (mirrors `__resetMalformedNoticeSeatAnnouncement`): never called by production code. */
286
334
  export declare function __resetMalformedDelegationSeatAnnouncement(): void;
287
335
  /**
@@ -1,4 +1,26 @@
1
1
  import { observeThenableRejection } from "./safe-notify.js";
2
+ export function emitsChildRowFrames(identity) {
3
+ return identity.isDelegatedChild;
4
+ }
5
+ export function taskStartedFrame(run, spawner, stamp) {
6
+ const { identity } = run;
7
+ return {
8
+ type: "task_started",
9
+ taskId: run.taskId,
10
+ sessionId: run.sessionId,
11
+ ...(spawner?.delegationTaskType !== undefined ? { taskType: spawner.delegationTaskType } : {}),
12
+ ...(spawner?.cycleSeq !== undefined ? { seq: spawner.cycleSeq } : {}),
13
+ ...(spawner?.parentTaskId !== undefined ? { parentTaskId: spawner.parentTaskId } : {}),
14
+ ...(spawner?.parentSessionId !== undefined ? { parentSessionId: spawner.parentSessionId } : {}),
15
+ ...(spawner?.rootSessionId !== undefined ? { rootSessionId: spawner.rootSessionId } : {}),
16
+ ...(identity.agentName ? { name: identity.agentName } : {}),
17
+ ...(spawner?.explicitAgentName !== undefined ? { spawnName: spawner.explicitAgentName } : {}),
18
+ ...(spawner?.spawnAgentType !== undefined ? { agentType: spawner.spawnAgentType } : {}),
19
+ model: run.model,
20
+ startedAt: Date.now(),
21
+ ...stamp,
22
+ };
23
+ }
2
24
  let malformedDelegationSeatAnnounced = false;
3
25
  export function __resetMalformedDelegationSeatAnnouncement() {
4
26
  malformedDelegationSeatAnnounced = false;
@@ -394,11 +394,13 @@ export interface EngineNotice {
394
394
  * rowCount, sessionId, runId }` (`rows` = the first eight non-built-in rows). Audience `user`.
395
395
  * - `"permission.rule_store_unavailable"` — the person's persisted permission rules could not be read (a read
396
396
  * failure, a corrupt or unreadable store, a read that did not answer in time), so that read's rows were SKIPPED:
397
- * none of their allow, ask or deny rows took part in the decisions it served — a deny row among them protected
398
- * nothing meanwhile — and no card is raised for it. `detail.at` names the read that found it first: `"prepare"`
399
- * (the path rows the run's file and shell checks take at run start — the next run reads them again) or `"call"`
400
- * (one call's own rule check — every later call reads again, so the rows apply as soon as a read succeeds). Once
401
- * per run, whichever read fails first; the trace keeps every read's line (`permission.rule_store_unreadable`).
397
+ * none of their allow, ask or deny rows took part in the decisions it served. At `"call"` the call asks a person
398
+ * meanwhile (a deny row among the rows nobody could read may forbid it; the class is `governance_unavailable`,
399
+ * which `bypassPermissions` keeps and a headless run refuses); at `"prepare"` the run's path tables lack the rows
400
+ * for the whole run. `detail.at` names the read that found it first: `"prepare"` (the path rows the run's file and
401
+ * shell checks take at run start — the next run reads them again) or `"call"` (one call's own rule check — every
402
+ * later call reads again, so the rows apply as soon as a read succeeds). Once per run, whichever read fails
403
+ * first; the trace keeps every read's line (`permission.rule_store_unreadable`).
402
404
  * `detail: { at, sessionId, runId }`. Audience `user`.
403
405
  * - `"permission.rule_row_dropped"` — the person's store served project-scoped rows whose scope root is not a
404
406
  * complete absolute path (only a foreign writer or a hand edit makes one; every engine write face refuses it): they
@@ -86,7 +86,8 @@ export declare function noteHostRewrite(seam: FrameSettlement, toolCallId: strin
86
86
  * carrier (the host's decision), withheld otherwise (owed again). */
87
87
  export declare function recordFrameReceipt(seam: FrameSettlement, carrier: string, committedContent: unknown, entryId: string): void;
88
88
  /** The carrier a committed frame was riding, as the harness reports it at the transcript append — a tool
89
- * result with the content it was committed with (after every result transformation). */
89
+ * result with the content it was committed with (after every result transformation); a user-attachment frame by
90
+ * its delivery record's key (`inputId`, `ordinal`). */
90
91
  export type CommittedFrameCarrier = {
91
92
  kind: "engine_note";
92
93
  payload: unknown;
@@ -94,4 +95,8 @@ export type CommittedFrameCarrier = {
94
95
  kind: "tool_result";
95
96
  toolCallId: string;
96
97
  content: unknown;
98
+ } | {
99
+ kind: "attachment";
100
+ inputId: string;
101
+ ordinal: number;
97
102
  };
@@ -5,17 +5,26 @@ import { normalizeReadRootCandidate } from "./read-root-candidate.js";
5
5
  import { traceHookCrash } from "./gate-crash-trace.js";
6
6
  import { writeProtectionMandated, writeProtectionTightenDecision } from "./write-protect.js";
7
7
  import { createGateDeadline, awaitWriteProtectionJudgment, gateWaitFailureOf, withGateTimeout } from "./gate-bounded-await.js";
8
+ import { shellReadFaceWords } from "../tools/fs/shell-read-face.js";
9
+ import { failedJudgmentDeny } from "./runner/policy-throw.js";
8
10
  import { thrownText } from "./thrown-value.js";
9
11
  import { refuseOutOfContractDecision } from "./tool-policy.js";
12
+ import { applyUnansweredSilence, silencedBy } from "./shell-read-silence.js";
10
13
  import { asksUnstated, permissionSeatAt, unstatedAskMessage } from "./mode-policy.js";
11
14
  export async function runGateFold(pass) {
12
15
  const { input, toolName, toolCallId, callSignal, ledger, notifier, adjudicate } = pass;
13
16
  const boundedAwaitDeadlineMs = createGateDeadline(input, notifier);
14
17
  pass.req = { toolName, args: pass.currentInput, toolCallId, ...(pass.req.face !== undefined ? { face: pass.req.face } : {}) };
15
- const stamped = input.stampCallTarget?.(pass.req, callSignal ?? input.abortSignal);
16
- if (stamped !== undefined)
17
- pass.req = await stamped;
18
- pass.decision = adjudicate ? refuseOutOfContractDecision(await (callSignal !== undefined ? adjudicate(pass.req, callSignal) : adjudicate(pass.req))) : { action: "allow" };
18
+ let unread;
19
+ try {
20
+ const stamped = input.stampCallTarget?.(pass.req, callSignal ?? input.abortSignal);
21
+ if (stamped !== undefined)
22
+ pass.req = await stamped;
23
+ }
24
+ catch (error) {
25
+ unread = { error };
26
+ }
27
+ pass.decision = unread !== undefined ? failedJudgmentDeny(unread.error, callSignal ?? input.abortSignal, (e) => traceHookCrash(input, e, notifier), "the call's target reading", { toolName, toolCallId }) : adjudicate ? refuseOutOfContractDecision(await (callSignal !== undefined ? adjudicate(pass.req, callSignal) : adjudicate(pass.req))) : { action: "allow" };
19
28
  {
20
29
  const read = ledger.fromDecision(pass.decision);
21
30
  pass.decision = read.decision;
@@ -180,9 +189,10 @@ export async function runGateFold(pass) {
180
189
  return await pass.afterFold(pass);
181
190
  }
182
191
  function shellReadAskMessage(toolName, verdict) {
192
+ const { verb, participle } = shellReadFaceWords(verdict.face);
183
193
  if (verdict.finding !== "outside_roots")
184
- return `tool "${toolName}" reads where the session's read boundary could not establish what is read — explicit approval required`;
185
- return `tool "${toolName}" ${verdict.face === "write" ? "writes" : "reads"} outside the directories allowed for this session — explicit approval required`;
194
+ return `tool "${toolName}" ${verb} where the session's read boundary could not establish what is ${participle} — explicit approval required`;
195
+ return `tool "${toolName}" ${verb} outside the directories allowed for this session — explicit approval required`;
186
196
  }
187
197
  function applyShellReadVerdict(pass, verdict, toolName, shellGated) {
188
198
  const account = verdict.sentence !== undefined && verdict.sentence.trim().length > 0 ? inlineUntrusted(verdict.sentence, PROBE_REASON_MAX) : undefined;
@@ -196,25 +206,26 @@ function applyShellReadVerdict(pass, verdict, toolName, shellGated) {
196
206
  pass.shellUnplacedOptions = verdict.unplacedOptions;
197
207
  if (verdict.finding === "unestablishable" || verdict.finding === "declined" || verdict.finding === "unanswered" || verdict.unestablished === true)
198
208
  pass.shellReadUnestablished = true;
209
+ if (verdict.disposition === "refuse") {
210
+ pass.decision = { action: "deny", message: verdict.sentence ?? `tool "${toolName}" reads what the session's read boundary refuses` };
211
+ pass.deniedBy = "read_boundary";
212
+ if (verdict.details !== undefined)
213
+ pass.denyDetails = verdict.details;
214
+ return;
215
+ }
216
+ if (verdict.finding === "unanswered") {
217
+ applyUnansweredSilence(pass, verdict, { message: shellReadAskMessage(toolName, verdict), account, tightenedBy: shellGated ? "shell_gate" : "safety" });
218
+ return;
219
+ }
199
220
  switch (verdict.disposition) {
200
221
  case "vouch":
201
222
  return;
202
- case "refuse":
203
- pass.decision = { action: "deny", message: verdict.sentence ?? `tool "${toolName}" reads what the session's read boundary refuses` };
204
- pass.deniedBy = "read_boundary";
205
- if (verdict.details !== undefined)
206
- pass.denyDetails = verdict.details;
207
- return;
208
223
  case "mandated": {
209
224
  if (verdict.details !== undefined)
210
225
  pass.shellReadDetails = verdict.details;
211
- const unreadable = verdict.finding === "unanswered";
212
226
  if (pass.decision.action !== "allow") {
213
- if (pass.decision.action === "ask") {
214
- pass.decision = !unreadable
215
- ? { ...pass.decision, probeMandated: true }
216
- : { ...pass.decision, requiresRealApproval: true, ...(pass.decision.guard === undefined ? { guard: "unverifiable_shell_text" } : { foldedAskClasses: [...new Set([...(pass.decision.foldedAskClasses ?? []), "unverifiable_shell_text"])] }) };
217
- }
227
+ if (pass.decision.action === "ask")
228
+ pass.decision = { ...pass.decision, probeMandated: true, ...silencedBy(verdict, pass.decision) };
218
229
  return;
219
230
  }
220
231
  const candidate = normalizeReadRootCandidate(verdict.readRootCandidate);
@@ -222,7 +233,8 @@ function applyShellReadVerdict(pass, verdict, toolName, shellGated) {
222
233
  action: "ask",
223
234
  message: shellReadAskMessage(toolName, verdict),
224
235
  decisionReason: "safety",
225
- ...(unreadable ? { requiresRealApproval: true, guard: "unverifiable_shell_text" } : { probeMandated: true }),
236
+ probeMandated: true,
237
+ ...silencedBy(verdict, {}),
226
238
  ...(account !== undefined ? { probeReason: account } : {}),
227
239
  ...(candidate !== undefined ? { probeReadRootCandidate: candidate } : {}),
228
240
  };
@@ -235,12 +247,15 @@ function applyShellReadVerdict(pass, verdict, toolName, shellGated) {
235
247
  verdict.admit?.();
236
248
  if (verdict.details !== undefined)
237
249
  pass.shellReadDetails = verdict.details;
250
+ if (pass.decision.action === "ask")
251
+ pass.decision = { ...pass.decision, ...silencedBy(verdict, pass.decision) };
238
252
  if (pass.decision.action === "allow") {
239
253
  const candidate = normalizeReadRootCandidate(verdict.readRootCandidate);
240
254
  pass.decision = {
241
255
  action: "ask",
242
256
  message: shellReadAskMessage(toolName, verdict),
243
257
  decisionReason: "safety",
258
+ ...silencedBy(verdict, {}),
244
259
  ...(account !== undefined ? { probeReason: account } : {}),
245
260
  ...(candidate !== undefined ? { probeReadRootCandidate: candidate } : {}),
246
261
  };