@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
@@ -1,14 +1,15 @@
1
1
  /**
2
2
  * The `tool_end` output/structured projection, verbatim from
3
3
  * `runtask.ts`'s runLocked R4 phase. Pure functions over the harness `AgentToolResult`: zero closure
4
- * state, zero runtask coupling — the ONLY module-level input is the size cap below. Event-sequence
5
- * snapshot pin (recorded before/after the move, byte-equal) guards the projection semantics.
4
+ * state, zero runtask coupling — the module-level inputs are the text cap below and the shared media budget default
5
+ * (`mediaBudgetBytes`); the leg hands in its own `mediaByteCapBytes`.
6
6
  */
7
- export declare const toolOutputFrom: (result: unknown) => {
7
+ type ToolOutput = {
8
8
  output: unknown;
9
9
  truncated: boolean;
10
10
  totalChars?: number;
11
- } | undefined;
11
+ };
12
+ export declare const toolOutputFrom: (result: unknown, mediaByteCapBytes: number | undefined) => ToolOutput | undefined;
12
13
  /** The `tool_end.structured` projection over a harness tool result: the CC card when `details.type` names a card
13
14
  * the leg admits, else undefined. Minted per leg by {@link createStructuredProjector}; every body projection takes it
14
15
  * as a parameter — there is no module-level card set to fall back to. */
@@ -23,3 +24,4 @@ export declare function createStructuredProjector(opts?: {
23
24
  declaredCards?: Iterable<string>;
24
25
  onUndeclared?: (type: string) => void;
25
26
  }): StructuredProjector;
27
+ export {};
@@ -1,62 +1,81 @@
1
+ import { isTextBlock, renderContentBlock } from "../../internal/llm.js";
2
+ import { imageBytes, mediaBudgetBytes } from "../media-byte-cap.js";
1
3
  import { engineCardTypes } from "../tool-registry.js";
2
4
  import { readMember } from "../thrown-value.js";
3
5
  const MAX_TOOL_END_OUTPUT_CHARS = 16384;
4
- const safeJsonLength = (v) => {
5
- try {
6
- return JSON.stringify(v)?.length;
6
+ const marker = (n) => `…[+${n} chars truncated]`;
7
+ const isMediaBlock = (b) => {
8
+ const block = b;
9
+ return (block?.type === "image" || block?.type === "document") && typeof block.data === "string";
10
+ };
11
+ const ownChars = (b) => isTextBlock(b) ? b.text.length + (JSON.stringify({ ...b, type: undefined, text: undefined })?.length ?? 2) - 2 : isMediaBlock(b) ? b.data.length : (JSON.stringify(b)?.length ?? 0);
12
+ const boundTextLane = (blocks, lane) => {
13
+ const sized = lane.map((i) => ({ i, size: ownChars(blocks[i]) }));
14
+ const stream = sized.reduce((n, { size }, k) => n + size + (k > 0 ? 1 : 0), 0);
15
+ const forms = new Map();
16
+ let used = 0;
17
+ for (const [k, { i, size }] of sized.entries()) {
18
+ const sep = k > 0 ? 1 : 0;
19
+ if (used + sep + size <= MAX_TOOL_END_OUTPUT_CHARS) {
20
+ used += sep + size;
21
+ forms.set(i, blocks[i]);
22
+ continue;
23
+ }
24
+ const shown = renderContentBlock(blocks[i]).slice(0, Math.max(0, MAX_TOOL_END_OUTPUT_CHARS - used - sep));
25
+ const notShown = stream - Math.min(MAX_TOOL_END_OUTPUT_CHARS, used + sep) - (isTextBlock(blocks[i]) ? shown.length : 0);
26
+ forms.set(i, { type: "text", text: shown + marker(notShown) });
27
+ return { forms, cut: true };
7
28
  }
8
- catch {
9
- return undefined;
29
+ return { forms, cut: false };
30
+ };
31
+ const wireBytes = (b) => Buffer.byteLength(JSON.stringify(b)) + 1;
32
+ const SUMMARY_RESERVE = wireBytes({ type: "omitted", blocks: Number.MAX_SAFE_INTEGER, bytes: Number.MAX_SAFE_INTEGER, truncated: true }) + 2;
33
+ const fitMedia = (media, budget) => {
34
+ let used = 0;
35
+ let degraded = false;
36
+ const blocks = [];
37
+ for (const [i, m] of media.entries()) {
38
+ const cost = used + m.data.length >= budget ? Infinity : wireBytes(m);
39
+ const kept = used + cost <= budget;
40
+ const out = kept ? m : { type: m.type, ...(typeof m.mimeType === "string" && m.mimeType.length <= 255 ? { mimeType: m.mimeType } : {}), bytes: imageBytes(m), truncated: true };
41
+ const outCost = kept ? cost : wireBytes(out);
42
+ degraded ||= !kept;
43
+ if (used + outCost > budget) {
44
+ const rest = media.slice(i);
45
+ blocks.push({ type: "omitted", blocks: rest.length, bytes: rest.reduce((n, r) => n + imageBytes(r), 0), truncated: true });
46
+ break;
47
+ }
48
+ used += outCost;
49
+ blocks.push(out);
10
50
  }
51
+ return { blocks, degraded };
11
52
  };
12
- const trunc = (s) => ({
13
- text: s.slice(0, MAX_TOOL_END_OUTPUT_CHARS) + `…[+${s.length - MAX_TOOL_END_OUTPUT_CHARS} chars truncated]`,
14
- totalChars: s.length,
15
- });
16
- export const toolOutputFrom = (result) => {
17
- const content = result !== null && typeof result === "object" ? result.content : result;
18
- if (content === undefined)
19
- return undefined;
20
- if (typeof content === "string") {
21
- if (content.length <= MAX_TOOL_END_OUTPUT_CHARS)
53
+ const project = (content, mediaByteCapBytes) => {
54
+ if (!Array.isArray(content)) {
55
+ const s = typeof content === "string" ? content : (JSON.stringify(content) ?? "");
56
+ if (s.length <= MAX_TOOL_END_OUTPUT_CHARS)
22
57
  return { output: content, truncated: false };
23
- const t = trunc(content);
24
- return { output: t.text, truncated: true, totalChars: t.totalChars };
58
+ return { output: s.slice(0, MAX_TOOL_END_OUTPUT_CHARS) + marker(s.length - MAX_TOOL_END_OUTPUT_CHARS), truncated: true, totalChars: s.length };
25
59
  }
26
- let serialized;
60
+ const media = content.map(isMediaBlock);
61
+ const text = boundTextLane(content, content.flatMap((_, i) => (media[i] ? [] : [i])));
62
+ const limit = mediaBudgetBytes(mediaByteCapBytes);
63
+ const textBytes = [...text.forms.values()].reduce((n, f) => n + wireBytes(f), 0);
64
+ const fit = fitMedia(content.filter((_, i) => media[i]), limit === Infinity ? Infinity : Math.ceil((limit * 4) / 3) - SUMMARY_RESERVE - textBytes);
65
+ if (!text.cut && !fit.degraded)
66
+ return { output: content, truncated: false };
67
+ let next = 0;
68
+ const output = content.flatMap((_, i) => (media[i] ? (fit.blocks[next++] ?? []) : text.forms.has(i) ? [text.forms.get(i)] : []));
69
+ return { output, truncated: true, totalChars: content.reduce((n, b) => n + ownChars(b), 0) };
70
+ };
71
+ export const toolOutputFrom = (result, mediaByteCapBytes) => {
27
72
  try {
28
- serialized = JSON.stringify(content) ?? "";
73
+ const content = result !== null && typeof result === "object" ? result.content : result;
74
+ return content === undefined ? undefined : project(content, mediaByteCapBytes);
29
75
  }
30
76
  catch {
31
- return { output: "[unserializable tool output]", truncated: false };
32
- }
33
- if (serialized.length <= MAX_TOOL_END_OUTPUT_CHARS)
34
- return { output: content, truncated: false };
35
- let anyNonText = false;
36
- let trueChars = 0;
37
- const raw = Array.isArray(content)
38
- ? content
39
- .map((b) => {
40
- const block = b;
41
- if (block?.type === "text" && typeof block.text === "string") {
42
- trueChars += block.text.length;
43
- return block.text;
44
- }
45
- anyNonText = true;
46
- trueChars += typeof block?.data === "string" ? block.data.length : (safeJsonLength(block) ?? 0);
47
- return `[${typeof block?.type === "string" ? block.type : "block"}${typeof block?.mimeType === "string" ? `: ${block.mimeType}` : ""}]`;
48
- })
49
- .join("\n")
50
- : serialized;
51
- const totalChars = Array.isArray(content) ? trueChars : serialized.length;
52
- if (raw.length > MAX_TOOL_END_OUTPUT_CHARS) {
53
- const t = trunc(raw);
54
- return { output: t.text, truncated: true, totalChars };
55
- }
56
- if (!anyNonText) {
57
- return { output: raw, truncated: false };
77
+ return { output: "[unserializable tool output]", truncated: true };
58
78
  }
59
- return { output: raw, truncated: true, totalChars };
60
79
  };
61
80
  const CC_DETAIL_TYPES = engineCardTypes();
62
81
  export function createStructuredProjector(opts = {}) {
@@ -852,8 +852,9 @@ export interface RunnerDeps {
852
852
  * family this seat complements, not replaces — BCE stays the fleet-row lane with registry `a*`
853
853
  * handles and ticks; this seat is the leg-identity lane), and workflow-spawned agents. Frames are
854
854
  * minted at the child leg's own runner chokepoint and carry the #281 件A identity envelope plus a
855
- * terminal status summary — see {@link DelegationLifecycleEvent} for the emission points, the
856
- * per-leg semantics and the recorded honest absences. Wire it once at Runner construction; a
855
+ * terminal status summary; the `spawn` phase is the projection of the child's `task_started` frame
856
+ * (once per cycle), the `terminal` phase is per leg — see {@link DelegationLifecycleEvent} for the
857
+ * emission points and the recorded honest absences. Wire it once at Runner construction; a
857
858
  * deployment needs NO RunInternals access to observe delegation any more. Observation only — a
858
859
  * throwing or rejecting observer is contained ({@link deliverDelegationLifecycle}) and never
859
860
  * faults the child run; a PRESENT non-function value here is announced once per process and the
@@ -1446,7 +1447,9 @@ export interface RunnerDeps {
1446
1447
  * CC 2.1.196 `tengu_media_byte_cap` parity). Image reads (`Read` → ImageContent) are per-image-capped but
1447
1448
  * accumulate across turns; this bounds the SUM in the request by stripping the OLDEST images (replaced with a
1448
1449
  * marker) — request-only, non-destructive (the durable session keeps the image; the model can re-Read it).
1449
- * Default {@link AGGREGATE_MEDIA_BUDGET_BYTES}; set `Infinity`/`0` to disable.
1450
+ * The same budget bounds the media of one `tool_end.output` (spent there as base64 on the wire, x 4/3); both readers
1451
+ * read it through `mediaBudgetBytes`. Default {@link AGGREGATE_MEDIA_BUDGET_BYTES}; set `Infinity`/`0` to disable. A
1452
+ * non-number, `NaN` or a negative is refused at `new Runner` (`config.invalid_argument`), never read as "no budget".
1450
1453
  */
1451
1454
  mediaByteCapBytes?: number;
1452
1455
  /** Optional telemetry fired when {@link mediaByteCapBytes} strips media in a request (CC's
@@ -2,8 +2,8 @@
2
2
  * design/177 — the SHARED memory-store seam (connected team/deployment memory libraries the model may
3
3
  * BROWSE and READ through two tools) and its model-facing constants.
4
4
  *
5
- * Upstream anchor: CC 2.1.223's `memory_list` / `memory_read` pair. The defining property of that face
6
- * is READ/WRITE ASYMMETRY: the model side has no write channel at all — write governance is an
5
+ * Upstream anchor: CC 2.1.223's `memory_list` / `memory_read` pair, whose defining property was READ/WRITE
6
+ * ASYMMETRY (no model-side write channel); CC no longer has that shape — 2.1.296 also mounts `yd` (cli296.js:242348) "memory_write" — and sema keeps the 223 one (KL-878): write governance is an
7
7
  * environment concern, and the tool face's entire participation in it is truthfully reporting each
8
8
  * store's `writable` bit. sema keeps that structure and pushes the whole credential/consent half onto
9
9
  * the HOST (BYOM: core holds no identity face), so this module's seam is a single pull-shaped snapshot
@@ -0,0 +1,23 @@
1
+ import type { ShellReadVerdict } from "../tools/fs/shell-read-check.js";
2
+ import type { GatePass } from "./runner/gate-exit.js";
3
+ import type { AskGuard } from "./tool-policy.js";
4
+ /** Whose silence a station's answer carries, each its own class on the question it keeps or mints: unread rows are a rule that could
5
+ * not be read (the rule lane's class); the finding `unanswered` is, unless rows are unread, the environment's silence — shell text the
6
+ * engine cannot read (a guard) — and the marks only pick whose. Either is a question only a person answers; `probe` follows the finding. */
7
+ export declare function silencedBy(verdict: ShellReadVerdict, d: {
8
+ readonly guards?: readonly AskGuard[];
9
+ readonly ruleUnreadable?: unknown;
10
+ }): {
11
+ ruleUnreadable?: "persisted_rows";
12
+ guards?: readonly AskGuard[];
13
+ requiresRealApproval?: true;
14
+ };
15
+ /** The finding `unanswered` IS the silence, applied once, before any disposition arm: a station — a host-mounted one included — may
16
+ * spell `vouch` or `mandated` beside it, and neither word vouches for what nobody judged. A deny stands; an ask keeps its words and
17
+ * gains the silence's class; an allow becomes the silence's question (`ask.message`), one only a person answers. Nothing is admitted,
18
+ * no `probe` is stamped and no grant is offered — the station made no finding. */
19
+ export declare function applyUnansweredSilence(pass: GatePass, verdict: ShellReadVerdict, ask: {
20
+ readonly message: string;
21
+ readonly account: string | undefined;
22
+ readonly tightenedBy: "shell_gate" | "safety";
23
+ }): void;
@@ -0,0 +1,18 @@
1
+ export function silencedBy(verdict, d) {
2
+ const environment = verdict.environmentSilent === true || (verdict.finding === "unanswered" && verdict.rowsUnread !== true);
3
+ const rows = verdict.rowsUnread === true && d.ruleUnreadable === undefined ? { ruleUnreadable: "persisted_rows" } : {};
4
+ const guards = environment && d.guards?.includes("unverifiable_shell_text") !== true ? { guards: [...(d.guards ?? []), "unverifiable_shell_text"] } : {};
5
+ return environment || verdict.rowsUnread === true ? { ...rows, ...guards, requiresRealApproval: true } : {};
6
+ }
7
+ export function applyUnansweredSilence(pass, verdict, ask) {
8
+ if (verdict.details !== undefined)
9
+ pass.shellReadDetails = verdict.details;
10
+ if (pass.decision.action === "ask") {
11
+ pass.decision = { ...pass.decision, ...silencedBy(verdict, pass.decision) };
12
+ return;
13
+ }
14
+ if (pass.decision.action !== "allow")
15
+ return;
16
+ pass.decision = { action: "ask", message: ask.message, decisionReason: "safety", ...silencedBy(verdict, {}), ...(ask.account !== undefined ? { probeReason: ask.account } : {}) };
17
+ pass.tightenedBy = ask.tightenedBy;
18
+ }
@@ -1,24 +1,40 @@
1
1
  /**
2
- * The synthetic-Read DENY judge — "would a `Read` of this file be refused outright?", asked without a call,
3
- * without an approver and without a hook. One judge, one set of layers, for every consumer that needs the
4
- * answer for a file the model did not itself ask to read (the changed-file notice today; a user's
5
- * attachment next). It answers in THREE states and never picks a direction:
2
+ * The synthetic-call judge — "would this call be refused outright?", asked without a model call, without an
3
+ * approver and without a hook. One judge, one set of layers, for every consumer that needs the answer for a file the
4
+ * model did not itself ask to touch. It has TWO FACES over one body (the layer walk below is written once):
5
+ *
6
+ * READ — a `Read` of a file the model did not ask for (the changed-file notice; a user's attachment). An ordinary
7
+ * `ask` — a person's own ask rule — is not a refusal: the person who would answer it is the one asking.
8
+ * WRITE — a `Write` of a path the engine itself writes in the background (the memory consolidation run). Every `ask`
9
+ * is an objection here: nobody is present to answer it, so a layer that would ask a person refuses the write.
10
+ *
11
+ * It never picks a direction. The read face answers in FOUR states:
6
12
  *
7
13
  * deny — some layer refuses it;
8
- * no_objection — every layer it consulted answered and none refused (an `ask` is not a refusal);
9
- * unanswerable — no layer refused, and at least one could not answer (see the table below).
14
+ * mandated_ask — no layer refuses it, and some layer would put the read to a person no rule may answer for
15
+ * (the ask's own mandate reading, {@link ruleClearanceOf}: a real-approval demand, the deployment's
16
+ * own question, an ancestor's, a hook's; or an organization rule's ask) — the person who must
17
+ * answer is not necessarily the one asking;
18
+ * no_objection — every layer it consulted answered and none objected;
19
+ * unanswerable — no layer objected, and at least one could not answer (see the table below).
20
+ *
21
+ * The write face answers the same four, with `ask` in place of `mandated_ask`: any layer's ask, mandated or not.
10
22
  *
11
- * The CONSUMER decides where `unanswerable` goes: a notice that merely tells the model a file changed
12
- * reads it as no objection (the worst case is a notice about a file it then cannot re-read); a face that
13
- * would put a file's contents in front of the model reads it as a refusal.
23
+ * ONE PRECEDENCE on both faces: a deny anywhere wins over a held ask, and a held ask over an unanswerable layer; the walk
24
+ * goes on past an ask (a later layer's deny is the stronger answer). The faces differ only in which asks are held. The
25
+ * CONSUMER decides where `mandated_ask` and `unanswerable` go: a notice that merely tells the model a file changed reads
26
+ * both as no objection (the worst case is a notice about a file it then cannot re-read); a face that would put a file's
27
+ * contents in front of the model reads both as a refusal; a background write reads everything but `no_objection` as a
28
+ * refusal. Every answer but `unanswerable` names the location the layers judged (`judged`, the canonical view of the
29
+ * stamped target, when a stamp was taken), so a consumer that resolved the file itself can tell the judged file from the
30
+ * one it opens.
14
31
  *
15
- * THE LAYERS (every one that can refuse a model's own `Read`): the run's skill deny rows; the
32
+ * THE LAYERS (every one that can refuse a model's own call): the run's skill deny rows; the
16
33
  * deny-narrowing fold (session rules, inherited ancestor rules, frozen constraint projections); the
17
34
  * deployment's caller policy; each live inherited parent policy; the person's persisted-rule lane; the org
18
- * lane. Not consulted, by design: PreToolUse hooks (the deployment's screening of a model's CALL), the
19
- * permission mode's write gate and the integrity guards (neither refuses a read), and the read-deny table
20
- * and containment roots (the path's own resolution judges those before this judge is asked). A deny
21
- * anywhere wins over an unanswerable layer elsewhere.
35
+ * lane. Not consulted, by design: PreToolUse hooks (the deployment's screening of a model's CALL), the permission mode's write
36
+ * gate, the integrity guards and the write-protection table (a live `Write` meets them; the read face never needs them and the
37
+ * write face's background run is not asked), and the read-deny table and containment roots (the path's own resolution judges those first).
22
38
  *
23
39
  * FAULT → STATE (the live gate's own readings, through the same functions — never a second copy):
24
40
  * | layer | fault | state |
@@ -28,11 +44,13 @@
28
44
  * | caller / inherited policy | its deny carries an engine settlement (an approval | unanswerable `approval_seat` |
29
45
  * | | seat that ended unanswered; in a combinePolicies | |
30
46
  * | | composition a rule deny anywhere is returned instead)| |
31
- * | persisted lane | store unreadable, throws, times out (readPersistedRuleLane ⇒ `{unreadable}`) | its rows take no part |
47
+ * | persisted lane | a store partition (or all of it) is unreadable, the lane throws, times out (readPersistedRuleLane ⇒ `{unreadable}`) | unanswerable `store_unreadable` (both faces: a row nobody could read may forbid the call) |
32
48
  * | persisted lane | the call cannot be read against the rows (`reason`) — applyPersistedTightening ⇒ `unreadable` | unanswerable `call_unreadable` |
33
49
  * | org lane | unavailable: throws, garbage (readOrgRuleLane) | unanswerable `org_unavailable` |
50
+ * | any policy layer | answers with a rewrite of the arguments (`updatedInput`) | unanswerable `rewritten` |
51
+ * | inherited parent policy | answers `ask` (the ancestor's approver's question) | mandated_ask (write: ask) |
34
52
  * | any layer | the judgment's budget (one dependency timeout, or a | unanswerable `timed_out` |
35
- * | | batch's shared deadline) is spent | (persisted: rows take no part)|
53
+ * | | batch's shared deadline) is spent | |
36
54
  *
37
55
  * NEVER AN APPROVER. A deployment policy (the caller's, an inherited parent's) is consulted with an
38
56
  * ALREADY-ABORTED signal and no approver seat: the `ToolPolicy.check` contract is that an aborted signal
@@ -45,32 +63,50 @@
45
63
  * the notice face stays silent. The engine's own layers (narrowing, skill rows, the two lanes) get the
46
64
  * run's real signal: they are pure matchers and store reads.
47
65
  *
48
- * THE PROBE'S IDENTITY. Every layer sees `toolName: "Read"`, the probed arguments, and the stable synthetic
49
- * call id `<face>-visibility-probe` ({@link syntheticReadProbeId}), so a deployment's audit can tell a
50
- * visibility probe from a model's call.
66
+ * THE PROBE'S IDENTITY. Every layer sees the probed tool's name (`Read` / `Write`), the probed arguments, and a stable
67
+ * synthetic call id (`<face>-visibility-probe` for a read, `<face>-write-probe` for a write; {@link syntheticReadProbeId},
68
+ * {@link syntheticWriteProbeId}), so a deployment's audit can tell a probe from a model's call. The write face stamps the
69
+ * facts the live gate stamps on a write (the roster's `Write` face, the tracked cwd and root, the advertised writable
70
+ * directories, the data root's exemption): a containment fence and a data-root exemption judge the probe as they judge the call.
51
71
  */
52
72
  import type { ToolCallRequest, ToolCallFace, ToolPolicy } from "./tool-policy.js";
53
- import type { CallTargetStamp } from "./effective-path-target.js";
73
+ import { type CallTargetStamp } from "./effective-path-target.js";
54
74
  import { type SkillGrantLane } from "./skill-grant.js";
55
75
  import type { OrgGateVerdict, PersistedRuleAnswer } from "./hooks.js";
56
76
  /** The layers the judge consults, as named in its verdicts. */
57
77
  export declare const SYNTHETIC_READ_LAYERS: readonly ["skill_rows", "narrowing", "caller_policy", "inherited_constraint", "persisted_rules", "org_rules"];
58
78
  export type SyntheticReadLayer = (typeof SYNTHETIC_READ_LAYERS)[number];
59
79
  /** Why a layer could not answer — the consumer's disclosure word. */
60
- export declare const SYNTHETIC_READ_UNANSWERABLE_CAUSES: readonly ["threw", "timed_out", "approval_seat", "call_unreadable", "org_unavailable"];
80
+ export declare const SYNTHETIC_READ_UNANSWERABLE_CAUSES: readonly ["threw", "timed_out", "approval_seat", "call_unreadable", "org_unavailable", "rewritten", "store_unreadable"];
61
81
  export type SyntheticReadUnanswerableCause = (typeof SYNTHETIC_READ_UNANSWERABLE_CAUSES)[number];
62
82
  export type SyntheticReadVerdict = {
63
83
  verdict: "deny";
64
84
  layer: SyntheticReadLayer;
65
85
  message?: string;
86
+ judged?: string;
87
+ } | {
88
+ verdict: "mandated_ask";
89
+ layer: SyntheticReadLayer;
90
+ message?: string;
91
+ judged?: string;
66
92
  } | {
67
93
  verdict: "no_objection";
94
+ judged?: string;
68
95
  } | {
69
96
  verdict: "unanswerable";
70
97
  layer: SyntheticReadLayer;
71
98
  cause: SyntheticReadUnanswerableCause;
72
99
  reason: string;
73
100
  };
101
+ /** The write face's answer: the read face's states with `ask` — any layer's ask, mandated or not — in place of `mandated_ask`. */
102
+ export type SyntheticWriteVerdict = Exclude<SyntheticReadVerdict, {
103
+ verdict: "mandated_ask";
104
+ }> | {
105
+ verdict: "ask";
106
+ layer: SyntheticReadLayer;
107
+ message?: string;
108
+ judged?: string;
109
+ };
74
110
  export interface SyntheticReadLayers {
75
111
  skillGrants?: Pick<SkillGrantLane, "denying">;
76
112
  denyNarrowingPolicy?: ToolPolicy;
@@ -97,9 +133,27 @@ export interface SyntheticReadCall {
97
133
  /** The live roster's face for `Read`, when the consumer has one. */
98
134
  face?: ToolCallFace;
99
135
  }
136
+ /** The path a synthetic `Write` probes. The request's other facts come from the judge's {@link SyntheticWriteFacts}. */
137
+ export interface SyntheticWriteCall {
138
+ file_path: string;
139
+ }
140
+ /** The facts the live gate stamps on a write, read at the moment of each judgment (the leg's cells move: the tracked cwd,
141
+ * the advertised-directory ledger). Every member is optional — a leg without hands has no roster face, cwd or root. */
142
+ export interface SyntheticWriteFacts {
143
+ /** The live roster's face for `Write` (a containment fence admits a write only by the tool's own declared path target). */
144
+ face?: ToolCallFace | undefined;
145
+ cwd?: string | undefined;
146
+ root?: string | undefined;
147
+ advertisedWritableDirs?: {
148
+ readonly roots: readonly string[];
149
+ } | undefined;
150
+ exemptRoots?: readonly string[] | undefined;
151
+ }
100
152
  /** The stable synthetic call id a probe carries: `<face>-visibility-probe`, `face` a lowercase word
101
153
  * (letters and hyphens) naming the consumer. */
102
154
  export declare function syntheticReadProbeId(face: string): string;
155
+ /** The write face's stable synthetic call id: `<face>-write-probe`, `face` the same lowercase word. */
156
+ export declare function syntheticWriteProbeId(face: string): string;
103
157
  /** One judgment's wait budget. `deadlineAt` (epoch ms) bounds the WHOLE judgment — every layer waits at most
104
158
  * what is left of it, and a layer reached after it has passed is not consulted (`timed_out`). Absent ⇒ the
105
159
  * dependency timeout from the start of this judgment. A consumer judging a batch passes one shared deadline,
@@ -108,4 +162,9 @@ export interface SyntheticReadBudget {
108
162
  deadlineAt?: number;
109
163
  }
110
164
  export type SyntheticReadJudge = (call: SyntheticReadCall, face: string, budget?: SyntheticReadBudget) => Promise<SyntheticReadVerdict>;
165
+ export type SyntheticWriteJudge = (call: SyntheticWriteCall, face: string, budget?: SyntheticReadBudget) => Promise<SyntheticWriteVerdict>;
166
+ /** The READ face: only a mandated ask is held (a person's own ask is that person's question). */
111
167
  export declare function createSyntheticReadJudge(layers: SyntheticReadLayers): SyntheticReadJudge;
168
+ /** The WRITE face: every ask is held as an objection (no approver is present), and a persisted store that cannot be read is
169
+ * unanswerable rather than silent. */
170
+ export declare function createSyntheticWriteJudge(layers: SyntheticReadLayers, facts: () => SyntheticWriteFacts): SyntheticWriteJudge;
@@ -1,20 +1,36 @@
1
- import { attestationOf, describeThrown, refuseOutOfContractDecision } from "./tool-policy.js";
1
+ import { callTargetReadingOf } from "./effective-path-target.js";
2
+ import { advertisedWritableRootsSeat, attestationOf, describeThrown, exemptRootsSeat, refuseOutOfContractDecision } from "./tool-policy.js";
3
+ import { ownedClearanceFacts, ruleClearanceOf } from "./ask-origin.js";
2
4
  import { skillRowRefusal } from "./skill-grant.js";
3
5
  import { applyPersistedTightening } from "./persisted-rule-arms.js";
4
6
  import { readOrgRuleLane, readPersistedRuleLane } from "./rule-lane-reads.js";
5
7
  import { ORG_ADJUDICATION_TIMEOUT_MS, settleOrgVerdictWithin } from "./permission-rule-org.js";
8
+ import { deepJsonEqual } from "./runner/decide-continuation.js";
6
9
  export const SYNTHETIC_READ_LAYERS = Object.freeze(["skill_rows", "narrowing", "caller_policy", "inherited_constraint", "persisted_rules", "org_rules"]);
7
- export const SYNTHETIC_READ_UNANSWERABLE_CAUSES = Object.freeze(["threw", "timed_out", "approval_seat", "call_unreadable", "org_unavailable"]);
10
+ export const SYNTHETIC_READ_UNANSWERABLE_CAUSES = Object.freeze(["threw", "timed_out", "approval_seat", "call_unreadable", "org_unavailable", "rewritten", "store_unreadable"]);
8
11
  export function syntheticReadProbeId(face) {
12
+ return `${probeFaceWord(face)}-visibility-probe`;
13
+ }
14
+ export function syntheticWriteProbeId(face) {
15
+ return `${probeFaceWord(face)}-write-probe`;
16
+ }
17
+ function probeFaceWord(face) {
9
18
  if (!/^[a-z][a-z-]*$/.test(face))
10
- throw new Error(`syntheticReadProbeId: the probe face must be a lowercase word (letters and hyphens), got ${JSON.stringify(face)}`);
11
- return `${face}-visibility-probe`;
19
+ throw new Error(`synthetic probe id: the probe face must be a lowercase word (letters and hyphens), got ${JSON.stringify(face)}`);
20
+ return face;
12
21
  }
13
22
  const ABORTED = AbortSignal.abort();
23
+ function sameArguments(value, probed) {
24
+ try {
25
+ return deepJsonEqual(value, probed);
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }
14
31
  export function createSyntheticReadJudge(layers) {
32
+ const judge = createProbeJudge(layers, "read");
15
33
  return async (call, face, budget) => {
16
- const deadlineAt = budget?.deadlineAt ?? Date.now() + ORG_ADJUDICATION_TIMEOUT_MS;
17
- const left = () => Math.max(0, Math.min(ORG_ADJUDICATION_TIMEOUT_MS, deadlineAt - Date.now()));
18
34
  const probe = {
19
35
  toolName: "Read",
20
36
  args: { file_path: call.file_path, ...(call.offset !== undefined ? { offset: call.offset } : {}), ...(call.limit !== undefined ? { limit: call.limit } : {}) },
@@ -22,32 +38,76 @@ export function createSyntheticReadJudge(layers) {
22
38
  ...(call.cwd !== undefined ? { cwd: call.cwd } : {}),
23
39
  ...(call.face !== undefined ? { face: call.face } : {}),
24
40
  };
41
+ const v = await judge(probe, budget);
42
+ return v.verdict === "ask" ? { ...v, verdict: "mandated_ask" } : v;
43
+ };
44
+ }
45
+ export function createSyntheticWriteJudge(layers, facts) {
46
+ const judge = createProbeJudge(layers, "write");
47
+ return async (call, face, budget) => {
48
+ const f = facts();
49
+ const probe = {
50
+ toolName: "Write",
51
+ args: { file_path: call.file_path, content: "" },
52
+ toolCallId: syntheticWriteProbeId(face),
53
+ ...(f.cwd !== undefined ? { cwd: f.cwd } : {}),
54
+ ...(f.root !== undefined ? { root: f.root } : {}),
55
+ ...(f.face !== undefined ? { face: f.face } : {}),
56
+ ...advertisedWritableRootsSeat(f.advertisedWritableDirs),
57
+ ...(f.exemptRoots !== undefined ? exemptRootsSeat(f.exemptRoots) : {}),
58
+ };
59
+ return judge(probe, budget);
60
+ };
61
+ }
62
+ function createProbeJudge(layers, kind) {
63
+ const everyAskHeld = kind === "write";
64
+ return async (probe, budget) => {
65
+ const deadlineAt = budget?.deadlineAt ?? Date.now() + ORG_ADJUDICATION_TIMEOUT_MS;
66
+ const left = () => Math.max(0, Math.min(ORG_ADJUDICATION_TIMEOUT_MS, deadlineAt - Date.now()));
67
+ const probedArgs = structuredClone(probe.args);
25
68
  const stamped = layers.stampCallTarget?.(probe, layers.signal);
26
69
  const req = stamped !== undefined ? await stamped : probe;
70
+ const view = callTargetReadingOf(req)?.protective;
71
+ const judged = view?.kind === "path" && view.canonical !== undefined ? { judged: view.canonical } : {};
27
72
  let unanswered;
73
+ let held;
28
74
  const cannot = (layer, cause, reason) => {
29
75
  unanswered ??= { verdict: "unanswerable", layer, cause, reason };
30
76
  };
77
+ const ask = (layer, message, mandated) => {
78
+ if (everyAskHeld || mandated)
79
+ held ??= { verdict: "ask", layer, ...(message !== undefined ? { message } : {}), ...judged };
80
+ };
31
81
  const spent = (layer) => {
32
82
  if (left() > 0)
33
83
  return false;
34
84
  cannot(layer, "timed_out", "the judgment's wait budget was spent before this layer was asked");
35
85
  return true;
36
86
  };
37
- const policyLayer = async (layer, policy, signal) => {
87
+ const policyLayer = async (layer, policy, signal, ancestorOwned = false) => {
38
88
  if (spent(layer))
39
89
  return undefined;
40
90
  const waitMs = left();
41
91
  let timedOut = false;
92
+ let thrown = undefined;
42
93
  const TIMEOUT = Symbol("timeout");
43
94
  let d;
44
95
  try {
45
- d = await settleOrgVerdictWithin(Promise.resolve().then(() => policy.check(req, signal)), TIMEOUT, { ...(layers.signal !== undefined ? { signal: layers.signal } : {}), timeoutMs: waitMs, onFallback: () => void (timedOut = true) });
96
+ d = await settleOrgVerdictWithin(Promise.resolve()
97
+ .then(() => policy.check(req, signal))
98
+ .catch((err) => {
99
+ thrown = { err };
100
+ return TIMEOUT;
101
+ }), TIMEOUT, { ...(layers.signal !== undefined ? { signal: layers.signal } : {}), timeoutMs: waitMs, onFallback: () => void (timedOut = true) });
46
102
  }
47
103
  catch (err) {
48
104
  cannot(layer, "threw", describeThrown(err));
49
105
  return undefined;
50
106
  }
107
+ if (thrown !== undefined) {
108
+ cannot(layer, "threw", describeThrown(thrown.err));
109
+ return undefined;
110
+ }
51
111
  if (d === TIMEOUT || timedOut) {
52
112
  cannot(layer, "timed_out", `no answer within ${waitMs}ms (or the run ended first)`);
53
113
  return undefined;
@@ -60,18 +120,25 @@ export function createSyntheticReadJudge(layers) {
60
120
  cannot(layer, "threw", describeThrown(err));
61
121
  return undefined;
62
122
  }
123
+ if (read.action !== "deny" && ((read.updatedInput !== undefined && !sameArguments(read.updatedInput, probedArgs)) || !sameArguments(req.args, probedArgs))) {
124
+ cannot(layer, "rewritten", "the layer would rewrite the call's arguments, so its answer is not about this call");
125
+ }
126
+ if (read.action === "ask") {
127
+ ask(layer, read.message, ancestorOwned || ruleClearanceOf(ownedClearanceFacts(read), {}).mandated);
128
+ return undefined;
129
+ }
63
130
  if (read.action !== "deny")
64
131
  return undefined;
65
132
  if (attestationOf(read, req)?.settlement !== undefined) {
66
- cannot(layer, "approval_seat", "the policy would ask an approver, who is never consulted for a visibility probe");
133
+ cannot(layer, "approval_seat", "the policy would ask an approver, who is never consulted for a synthetic probe");
67
134
  return undefined;
68
135
  }
69
- return { verdict: "deny", layer, ...(read.message !== undefined ? { message: read.message } : {}) };
136
+ return { verdict: "deny", layer, ...(read.message !== undefined ? { message: read.message } : {}), ...judged };
70
137
  };
71
138
  try {
72
139
  const refusal = skillRowRefusal(layers.skillGrants, req);
73
140
  if (refusal !== undefined)
74
- return { verdict: "deny", layer: "skill_rows", ...(refusal.message !== undefined ? { message: refusal.message } : {}) };
141
+ return { verdict: "deny", layer: "skill_rows", ...(refusal.message !== undefined ? { message: refusal.message } : {}), ...judged };
75
142
  }
76
143
  catch (err) {
77
144
  cannot("skill_rows", "threw", describeThrown(err));
@@ -82,14 +149,14 @@ export function createSyntheticReadJudge(layers) {
82
149
  return v;
83
150
  }
84
151
  if (layers.callerPolicy !== undefined) {
85
- const v = await policyLayer("caller_policy", layers.callerPolicy, ABORTED);
152
+ const v = await policyLayer("caller_policy", layers.callerPolicy, ABORTED, (layers.inheritedPolicies ?? []).includes(layers.callerPolicy));
86
153
  if (v !== undefined)
87
154
  return v;
88
155
  }
89
156
  for (const parent of layers.inheritedPolicies ?? []) {
90
157
  if (parent === layers.callerPolicy)
91
158
  continue;
92
- const v = await policyLayer("inherited_constraint", parent, ABORTED);
159
+ const v = await policyLayer("inherited_constraint", parent, ABORTED, true);
93
160
  if (v !== undefined)
94
161
  return v;
95
162
  }
@@ -97,17 +164,21 @@ export function createSyntheticReadJudge(layers) {
97
164
  const read = await readPersistedRuleLane(layers.persistedRuleLane, req, layers.signal, left());
98
165
  const tightened = applyPersistedTightening({ action: "allow" }, read).tightened;
99
166
  if (tightened === "deny")
100
- return { verdict: "deny", layer: "persisted_rules" };
101
- if (tightened === "unreadable")
102
- cannot("persisted_rules", "call_unreadable", read.reason ?? "the call could not be read against the rules");
167
+ return { verdict: "deny", layer: "persisted_rules", ...judged };
168
+ if (tightened === "ask")
169
+ ask("persisted_rules", undefined, false);
170
+ if (read.unreadable === true)
171
+ cannot("persisted_rules", read.reason !== undefined ? "call_unreadable" : "store_unreadable", read.reason ?? "a partition of the person's rule store could not be read, so whether a standing rule forbids this call is unknown");
103
172
  }
104
173
  if (layers.orgRuleLane !== undefined && !spent("org_rules")) {
105
174
  const org = await readOrgRuleLane(layers.orgRuleLane, req, layers.signal, left());
106
175
  if (org.status === "unavailable")
107
176
  cannot("org_rules", left() > 0 ? "org_unavailable" : "timed_out", org.disclosures.join("; ") || "organization governance could not be read");
108
177
  else if (org.verdict?.behavior === "deny")
109
- return { verdict: "deny", layer: "org_rules", message: `an organization policy rule (${org.verdict.rule}) denies it` };
178
+ return { verdict: "deny", layer: "org_rules", message: `an organization policy rule (${org.verdict.rule}) denies it`, ...judged };
179
+ else if (org.verdict?.behavior === "ask")
180
+ ask("org_rules", `an organization policy rule (${org.verdict.rule}) requires approval for it`, true);
110
181
  }
111
- return unanswered ?? { verdict: "no_objection" };
182
+ return held ?? unanswered ?? { verdict: "no_objection", ...judged };
112
183
  };
113
184
  }