@sema-agent/core 7.50.2 → 7.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/CHANGELOG.md +263 -0
  2. package/dist/agents/child-failure.d.ts +5 -16
  3. package/dist/agents/child-failure.js +4 -24
  4. package/dist/agents/subagent-steps.js +2 -2
  5. package/dist/agents/subagent.d.ts +4 -13
  6. package/dist/agents/subagent.js +94 -204
  7. package/dist/agents/tool-filter.d.ts +2 -2
  8. package/dist/brain/anthropic.d.ts +1 -1
  9. package/dist/brain/anthropic.js +7 -5
  10. package/dist/brain/open-responses.d.ts +1 -1
  11. package/dist/brain/open-responses.js +3 -2
  12. package/dist/brain/openai.d.ts +1 -1
  13. package/dist/brain/openai.js +3 -2
  14. package/dist/brain/request-route.d.ts +2 -2
  15. package/dist/brain/route-adjudicator.d.ts +16 -7
  16. package/dist/brain/route-adjudicator.js +23 -7
  17. package/dist/brain/route-conformance.js +23 -5
  18. package/dist/brain/stream-engine.js +9 -3
  19. package/dist/core/a2a.js +6 -8
  20. package/dist/core/allow-entry.d.ts +45 -26
  21. package/dist/core/allow-entry.js +48 -24
  22. package/dist/core/arg-summary.d.ts +8 -85
  23. package/dist/core/arg-summary.js +4 -386
  24. package/dist/core/ask-origin.d.ts +35 -33
  25. package/dist/core/ask-origin.js +9 -11
  26. package/dist/core/checkpoint-requirements.d.ts +6 -0
  27. package/dist/core/checkpoint-requirements.js +1 -0
  28. package/dist/core/checkpoint-store.d.ts +15 -34
  29. package/dist/core/checkpoint-store.js +5 -4
  30. package/dist/core/classifier-unavailable-arm.d.ts +12 -8
  31. package/dist/core/classifier-unavailable-arm.js +7 -2
  32. package/dist/core/credential-vocabulary.d.ts +3 -2
  33. package/dist/core/credential-vocabulary.js +27 -18
  34. package/dist/core/delegation-frames.d.ts +40 -15
  35. package/dist/core/delegation-frames.js +34 -0
  36. package/dist/core/engine-notice.d.ts +35 -11
  37. package/dist/core/gate-fold.js +9 -7
  38. package/dist/core/gate-lanes.js +6 -4
  39. package/dist/core/gate-outcome.d.ts +81 -15
  40. package/dist/core/gate-outcome.js +168 -67
  41. package/dist/core/governance-codes.d.ts +7 -1
  42. package/dist/core/governance-codes.js +9 -0
  43. package/dist/core/hooks.d.ts +4 -4
  44. package/dist/core/hooks.js +4 -3
  45. package/dist/core/mcp-connection-pool.d.ts +2 -0
  46. package/dist/core/mcp-connection-pool.js +2 -1
  47. package/dist/core/mcp-failure.js +2 -1
  48. package/dist/core/mcp-server-spec.d.ts +27 -5
  49. package/dist/core/mcp.d.ts +13 -38
  50. package/dist/core/mcp.js +28 -23
  51. package/dist/core/media-byte-cap.d.ts +25 -0
  52. package/dist/core/media-byte-cap.js +18 -2
  53. package/dist/core/memory-engine/consolidation-driver.js +1 -1
  54. package/dist/core/mode-policy.d.ts +25 -1
  55. package/dist/core/mode-policy.js +28 -13
  56. package/dist/core/own-property-receipt.d.ts +18 -0
  57. package/dist/core/own-property-receipt.js +39 -0
  58. package/dist/core/permission-rule-model.d.ts +32 -11
  59. package/dist/core/permission-rule-model.js +12 -7
  60. package/dist/core/persisted-rule-arms.d.ts +3 -3
  61. package/dist/core/prompt-section-seat.d.ts +54 -0
  62. package/dist/core/prompt-section-seat.js +49 -0
  63. package/dist/core/protocol-http-auth.d.ts +44 -0
  64. package/dist/core/protocol-http-auth.js +45 -0
  65. package/dist/core/read-face-seats.d.ts +30 -0
  66. package/dist/core/read-face-seats.js +38 -0
  67. package/dist/core/redaction-passes.d.ts +82 -0
  68. package/dist/core/redaction-passes.js +378 -0
  69. package/dist/core/remote-env.d.ts +4 -3
  70. package/dist/core/resume-startup-cause.d.ts +56 -0
  71. package/dist/core/resume-startup-cause.js +43 -0
  72. package/dist/core/rule-denial-sentence.d.ts +2 -2
  73. package/dist/core/runner/announce-once-ledger.js +2 -2
  74. package/dist/core/runner/classifier-ladder.d.ts +17 -13
  75. package/dist/core/runner/classifier-ladder.js +12 -10
  76. package/dist/core/runner/construction-doors.js +9 -20
  77. package/dist/core/runner/contracts.d.ts +4 -4
  78. package/dist/core/runner/gate-exit.js +2 -2
  79. package/dist/core/runner/memory-consolidation-write-judge.d.ts +53 -9
  80. package/dist/core/runner/memory-consolidation-write-judge.js +39 -1
  81. package/dist/core/runner/permission-rule-lanes.d.ts +8 -13
  82. package/dist/core/runner/person-path-rows.d.ts +4 -3
  83. package/dist/core/runner/policy-throw.js +4 -4
  84. package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -3
  85. package/dist/core/runner/prepare-caps-and-workflow.js +8 -6
  86. package/dist/core/runner/prepare-gate-stations.js +4 -4
  87. package/dist/core/runner/prepare-hands-readface.js +1 -19
  88. package/dist/core/runner/prepare-inherited-gate.js +10 -9
  89. package/dist/core/runner/prepare-policy-chain.d.ts +3 -5
  90. package/dist/core/runner/prepare-policy-chain.js +10 -13
  91. package/dist/core/runner/prepare-project-context.d.ts +4 -4
  92. package/dist/core/runner/prepare-project-context.js +2 -1
  93. package/dist/core/runner/prepare-prompt-assembly.js +1 -0
  94. package/dist/core/runner/prepare-prompt-inputs.d.ts +1 -1
  95. package/dist/core/runner/prepare-prompt-inputs.js +4 -4
  96. package/dist/core/runner/prepare-protocol-tools.d.ts +4 -3
  97. package/dist/core/runner/prepare-protocol-tools.js +3 -2
  98. package/dist/core/runner/prepare-task.js +6 -4
  99. package/dist/core/runner/prepare-turn-wiring.d.ts +16 -13
  100. package/dist/core/runner/prepare-turn-wiring.js +8 -7
  101. package/dist/core/runner/run-harness-handlers.js +2 -2
  102. package/dist/core/runner/run-leg.js +10 -9
  103. package/dist/core/runner/run-settle-and-teardown.js +5 -5
  104. package/dist/core/runner/run-turn-boundary.js +3 -2
  105. package/dist/core/runner/runtask.d.ts +1 -2
  106. package/dist/core/runner/runtask.js +8 -12
  107. package/dist/core/runner/session-rule-policy.d.ts +15 -11
  108. package/dist/core/runner/session-rule-policy.js +43 -33
  109. package/dist/core/runner/stream-lifecycle-verbs.js +2 -2
  110. package/dist/core/runner/stream-settle-backstop.js +2 -1
  111. package/dist/core/runner/sub-agent-deny-lists.d.ts +12 -8
  112. package/dist/core/runner/sub-agent-deny-lists.js +23 -12
  113. package/dist/core/runner/terminal-account.js +3 -3
  114. package/dist/core/runner/tool-output-projection.js +3 -8
  115. package/dist/core/runner/tool-result-media-seat.d.ts +19 -0
  116. package/dist/core/runner/tool-result-media-seat.js +13 -0
  117. package/dist/core/runner/turn-attachments.d.ts +13 -12
  118. package/dist/core/runner/turn-attachments.js +11 -7
  119. package/dist/core/runner-deps.d.ts +4 -3
  120. package/dist/core/safety-merge-corpus.js +21 -21
  121. package/dist/core/secret-env.js +1 -1
  122. package/dist/core/session-policy-store.d.ts +89 -72
  123. package/dist/core/session-policy-store.js +100 -93
  124. package/dist/core/shell-read-silence.d.ts +38 -4
  125. package/dist/core/shell-read-silence.js +102 -0
  126. package/dist/core/side-query.d.ts +7 -8
  127. package/dist/core/skill-grant-binding.d.ts +32 -11
  128. package/dist/core/skill-grant-binding.js +11 -3
  129. package/dist/core/skill-grant.d.ts +4 -3
  130. package/dist/core/skill-grant.js +5 -4
  131. package/dist/core/skills-directory.d.ts +12 -1
  132. package/dist/core/skills-directory.js +17 -8
  133. package/dist/core/store-contracts/checkpoint-store-contract.d.ts +2 -0
  134. package/dist/core/store-contracts/checkpoint-store-contract.js +3 -0
  135. package/dist/core/swappable-deps.d.ts +15 -1
  136. package/dist/core/swappable-deps.js +25 -1
  137. package/dist/core/synthetic-read-verdict.d.ts +16 -14
  138. package/dist/core/synthetic-read-verdict.js +28 -8
  139. package/dist/core/task-notification.d.ts +13 -3
  140. package/dist/core/task-notification.js +12 -1
  141. package/dist/core/task-spec.d.ts +14 -15
  142. package/dist/core/tool-name-readings.d.ts +0 -4
  143. package/dist/core/tool-name-readings.js +0 -1
  144. package/dist/core/tool-policy.d.ts +40 -9
  145. package/dist/core/tool-policy.js +25 -7
  146. package/dist/core/types.d.ts +3 -3
  147. package/dist/core/types.js +1 -1
  148. package/dist/core/untrusted-egress.d.ts +60 -11
  149. package/dist/core/untrusted-egress.js +120 -9
  150. package/dist/core/untrusted-text.js +16 -0
  151. package/dist/core/wiring-manifest.d.ts +22 -8
  152. package/dist/core/wiring-manifest.js +12 -4
  153. package/dist/engine/harness/agent-harness.d.ts +2 -1
  154. package/dist/engine/harness/agent-harness.js +4 -1
  155. package/dist/engine/harness/types.d.ts +3 -0
  156. package/dist/engine/llm/types.d.ts +7 -9
  157. package/dist/engine/loop/agent-loop.d.ts +8 -3
  158. package/dist/engine/loop/agent-loop.js +26 -9
  159. package/dist/engine/loop/types.d.ts +10 -0
  160. package/dist/index.d.ts +17 -5
  161. package/dist/index.js +14 -5
  162. package/dist/orchestration/run-workflow-tool.js +8 -8
  163. package/dist/orchestration/workflow.js +2 -2
  164. package/dist/prompt-assembly/assemble.d.ts +3 -1
  165. package/dist/prompt-assembly/assemble.js +6 -0
  166. package/dist/prompts/cowork.d.ts +42 -0
  167. package/dist/prompts/cowork.js +529 -0
  168. package/dist/prompts/default.d.ts +14 -1
  169. package/dist/prompts/default.js +3 -0
  170. package/dist/stores/file/session-policy-store.d.ts +6 -3
  171. package/dist/stores/file/session-policy-store.js +5 -16
  172. package/dist/tools/artifact/artifact-tool.js +2 -0
  173. package/dist/tools/fs/image-admission.d.ts +57 -4
  174. package/dist/tools/fs/image-admission.js +85 -0
  175. package/dist/tools/fs/path-containment.d.ts +2 -0
  176. package/dist/tools/fs/path-containment.js +5 -0
  177. package/dist/tools/fs/shell-read-check.d.ts +31 -6
  178. package/dist/tools/fs/shell-read-check.js +2 -4
  179. package/dist/tools/fs/shell-read-environment.d.ts +10 -25
  180. package/dist/tools/fs/shell-read-environment.js +6 -18
  181. package/dist/tools/fs/shell-read-establish.d.ts +3 -2
  182. package/dist/tools/fs/shell-read-establish.js +5 -5
  183. package/dist/tools/fs/shell-read-judge.d.ts +38 -8
  184. package/dist/tools/fs/shell-read-judge.js +12 -10
  185. package/dist/tools/web.js +2 -2
  186. package/package.json +1 -1
  187. package/test/export-surface.snapshot.json +94 -10
  188. package/wire-changes/core@7.51.0.json +2421 -0
@@ -1,6 +1,6 @@
1
1
  import { readFileSync, readdirSync, unlinkSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { assertSessionRuleNamesCurrent, loosenReasons, normalizeRules, readMountedToolNames, stripRev, SessionPolicyError, } from "../../core/session-policy-store.js";
3
+ import { judgeSessionRulesWrite, readMountedToolNames, readSessionRulesShape, SessionPolicyError, } from "../../core/session-policy-store.js";
4
4
  import { atomicWriteFile, ensureDir, sanitizeScope } from "./fs-atomic.js";
5
5
  import { guardCorruptSink } from "./corrupt-read-sink.js";
6
6
  import { assertAdoptionBootGate, readRootAdoptionFile } from "./adoption/marker.js";
@@ -52,13 +52,12 @@ export function judgeSessionPolicyRowShape(row) {
52
52
  return bad("rev is not a positive number");
53
53
  if (row.gen !== undefined && (typeof row.gen !== "number" || !Number.isInteger(row.gen) || row.gen < 0))
54
54
  return bad("gen is not a whole number");
55
- const strArr = (x) => x === undefined || (Array.isArray(x) && x.every((e) => typeof e === "string"));
56
- if (!strArr(row.toolAllow) || !strArr(row.toolDeny) || !strArr(row.commandAllow) || !strArr(row.commandDeny) || !strArr(row.allowDirs)) {
57
- return bad("malformed rule field");
58
- }
59
55
  const { __sid: _sid, __principal: _principal, ...clean } = row;
60
56
  void _sid;
61
57
  void _principal;
58
+ const shaped = readSessionRulesShape(clean);
59
+ if ("fault" in shaped)
60
+ return bad(shaped.sentence);
62
61
  return { rules: clean };
63
62
  }
64
63
  export function attributeSessionPolicyRow(dir, file, row) {
@@ -184,17 +183,7 @@ export class FileSessionPolicyStore {
184
183
  }
185
184
  async putRules(sessionId, principal, rules, opts) {
186
185
  const prior = this.read(sessionId, principal);
187
- if (opts?.expectedRev !== undefined && (prior?.rev ?? 0) !== opts.expectedRev) {
188
- throw new SessionPolicyError("conflict", `session rules rev mismatch (expected ${opts.expectedRev}, have ${prior?.rev ?? 0})`);
189
- }
190
- const clean = normalizeRules(stripRev(rules));
191
- assertSessionRuleNamesCurrent(clean, this.mountedToolNames);
192
- if (prior && opts?.operator !== true) {
193
- const loosens = loosenReasons(prior, clean, this.mountedToolNames);
194
- if (loosens.length > 0) {
195
- throw new SessionPolicyError("loosen_forbidden", `a non-operator session-rule write may only tighten; refused: ${loosens.join("; ")}`);
196
- }
197
- }
186
+ const clean = judgeSessionRulesWrite(prior, rules, opts, this.mountedToolNames);
198
187
  const mark = this.readMark(sessionId, principal);
199
188
  if (mark?.__lineage === "lost") {
200
189
  throw new SessionPolicyError("corrupt", `session rules for ${JSON.stringify(sessionId)} have no lineage evidence (the erase left a lost-lineage mark) — refusing to restart the key's lineage by guessing; delete the mark to restart it`);
@@ -5,6 +5,7 @@ import { Type } from "typebox";
5
5
  import { ARTIFACT_ACTION_CONCURRENCY_SAFE, ARTIFACT_COMMIT_THROW_OUTCOME, ARTIFACT_FAILURE_RESULT_CODE, ARTIFACT_HOST_ERROR_CODE, ARTIFACT_LIST_SCOPES, ARTIFACT_PLANE_CONTENT_ORIGIN, ARTIFACT_PUBLISH_CONTEXTS, } from "../../core/artifact-host.js";
6
6
  import { ARTIFACT_TOOL_NAME, toolFace } from "../../core/tool-catalog-entries.js";
7
7
  import { resolveAsk } from "../../core/tool-policy.js";
8
+ import { NOT_BINDABLE } from "../../core/skill-grant-binding.js";
8
9
  import { approvalCardOf } from "../../core/tool-roster.js";
9
10
  import { errorResult } from "../../core/tools.js";
10
11
  import { inlineUntrusted } from "../../core/untrusted-text.js";
@@ -345,6 +346,7 @@ export function createArtifactTool(opts) {
345
346
  message: `Read a shared artifact: ${inlineUntrusted(url, 300)}${title}${version} — an artifact another person shared with this user; approving covers re-reads of it for the rest of this run.`,
346
347
  requiresRealApproval: true,
347
348
  approvalCard: approvalCardOf({ toolName: ARTIFACT_TOOL_NAME }, toolFace("artifact").renderHints?.approvalCard),
349
+ ...NOT_BINDABLE,
348
350
  }, onAsk, ctx.signal);
349
351
  if (resolved.action !== "allow")
350
352
  return { ok: false, reason: resolved.message ?? "not approved" };
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The ONE admission an image's bytes pass before they reach the model as an `ImageContent` block —
3
- * whether a file supplied them (the Read tool's image lane) or a caller did (`TaskSpec.images`' inline
4
- * arm). Four steps, in this order:
3
+ * whether a file supplied them (the Read tool's image lane), a caller did (`TaskSpec.images`' inline
4
+ * arm, a user attachment) or a tool returned them (the tool-result seat below). Four steps, in this order:
5
5
  *
6
6
  * ① the claimed type is one the content API accepts (png / jpeg / gif / webp — the four
7
7
  * {@link imageMagicMatches} can verify);
@@ -14,10 +14,19 @@
14
14
  *
15
15
  * The answer is structured; each carrier words its own refusal (the Read tool speaks to the model, the
16
16
  * task door to the caller). The block it admits is built from the bytes it checked — never from a
17
- * caller's string it did not decode.
17
+ * caller's string it did not decode. A document's bytes pass the same shape of admission
18
+ * ({@link admitDocumentBytes}: `application/pdf`, the whole-send cap, the `%PDF-` signature).
19
+ *
20
+ * THE TOOL-RESULT SEAT ({@link admitToolResultMedia}): every executed call's result passes the loop's one
21
+ * post-execution half (`finalizeToolOutcome`) — an engine tool's, an MCP tool's converted content, a host
22
+ * tool's own return, a resumed approved call's, and whatever a `tool_result` hook put in their place — so
23
+ * the media blocks of every supply are admitted there, once, after the last hand that can change them and
24
+ * before the frame and the transcript read them. A block whose bytes the admission refuses becomes a text
25
+ * block that names the tool, the refusal and the size — never the bytes, and never silently nothing.
18
26
  */
19
- import type { ImageContent } from "../../internal/llm.js";
27
+ import type { DocumentContent, ImageContent } from "../../internal/llm.js";
20
28
  import { type ImageDimensions, type ImageDownsampler } from "../../core/mcp.js";
29
+ import { type MediaBlock } from "../../core/media-byte-cap.js";
21
30
  /** The image types the content API accepts — the only ones a signature check can verify. */
22
31
  export declare const ADMITTED_IMAGE_MIME_TYPES: readonly ["image/png", "image/jpeg", "image/gif", "image/webp"];
23
32
  /** The raw-byte read cap for an image, with or without a downsampler. */
@@ -49,3 +58,47 @@ export type ImageAdmission = {
49
58
  base64: string;
50
59
  };
51
60
  export declare function admitImageBytes(bytes: Uint8Array, claimedMime: string, downsampler: ImageDownsampler | undefined): Promise<ImageAdmission>;
61
+ /** The document types the content API accepts — the one a signature check can verify. */
62
+ export declare const ADMITTED_DOCUMENT_MIME_TYPES: readonly ["application/pdf"];
63
+ export type DocumentAdmission = {
64
+ ok: true;
65
+ block: DocumentContent;
66
+ originalSize: number;
67
+ } | {
68
+ ok: false;
69
+ reason: "mime_unsupported" | "magic_mismatch";
70
+ rawBytes: number;
71
+ } | {
72
+ ok: false;
73
+ reason: "raw_too_large";
74
+ rawBytes: number;
75
+ cap: number;
76
+ };
77
+ /** A document's bytes: the claimed type is `application/pdf`, the raw bytes are within the whole-send cap
78
+ * ({@link PDF_TARGET_RAW_SIZE}, the Read tool's), and they start with `%PDF-`. */
79
+ export declare function admitDocumentBytes(bytes: Uint8Array, claimedMime: string): DocumentAdmission;
80
+ /** Why the tool-result seat refused a media block: the admissions' own words (an image's four — a document's are among
81
+ * them), or `undecodable` — the block carried no base64 string to decode. */
82
+ export type MediaRefusalReason = "undecodable" | "mime_unsupported" | "raw_too_large" | "magic_mismatch" | "base64_too_large";
83
+ /** One media block the seat refused: its type, the refusal, its decoded size, the bound it exceeded (the raw-byte cap for
84
+ * `raw_too_large`, the base64 length limit for `base64_too_large`), and its media type when it can be one. */
85
+ export interface ToolResultMediaRefusal {
86
+ type: MediaBlock["type"];
87
+ reason: MediaRefusalReason;
88
+ bytes: number;
89
+ limit?: number;
90
+ mimeType?: string;
91
+ }
92
+ /** What the seat made of one result's content: every media block rebuilt from its checked bytes or replaced, in its slot,
93
+ * by its placeholder; and the refusals. */
94
+ export interface ToolResultMediaAdmission {
95
+ content: unknown[];
96
+ refused: ToolResultMediaRefusal[];
97
+ }
98
+ /**
99
+ * The tool-result seat's admission over one executed call's content: `undefined` when no block claims a media type (the
100
+ * content stands as it is, and nothing is awaited); otherwise every block is read once into a new array, each media
101
+ * block admitted — rebuilt from the bytes it checked, or replaced in its slot by a placeholder naming `toolName` and the
102
+ * refusal. Total: a content whose blocks cannot be read is not media this seat can admit or refuse, and is left as it is.
103
+ */
104
+ export declare function admitToolResultMedia(content: unknown, toolName: string): Promise<ToolResultMediaAdmission> | undefined;
@@ -1,6 +1,9 @@
1
1
  import { MCP_IMAGE_MAX_BASE64 } from "../../core/mcp.js";
2
+ import { claimedMediaType } from "../../core/media-byte-cap.js";
3
+ import { readMember } from "../../core/thrown-value.js";
2
4
  import { imageMagicMatches } from "./safety.js";
3
5
  import { MAX_IMAGE_READ_BYTES, MAX_IMAGE_DOWNSAMPLE_INPUT_BYTES } from "./fs-shared.js";
6
+ import { PDF_TARGET_RAW_SIZE, pdfMagicMatches } from "./pdf.js";
4
7
  export const ADMITTED_IMAGE_MIME_TYPES = Object.freeze(["image/png", "image/jpeg", "image/gif", "image/webp"]);
5
8
  export function imageReadCap(downsampler) {
6
9
  return downsampler ? MAX_IMAGE_DOWNSAMPLE_INPUT_BYTES : MAX_IMAGE_READ_BYTES;
@@ -34,3 +37,85 @@ export async function admitImageBytes(bytes, claimedMime, downsampler) {
34
37
  (dimensions.originalWidth !== dimensions.displayWidth || dimensions.originalHeight !== dimensions.displayHeight);
35
38
  return { ok: true, block: { type: "image", data: base64, mimeType }, originalSize: rawBytes, ...(dimensions !== undefined ? { dimensions } : {}), resized };
36
39
  }
40
+ export const ADMITTED_DOCUMENT_MIME_TYPES = Object.freeze(["application/pdf"]);
41
+ export function admitDocumentBytes(bytes, claimedMime) {
42
+ const rawBytes = bytes.byteLength;
43
+ if (!ADMITTED_DOCUMENT_MIME_TYPES.includes(claimedMime))
44
+ return { ok: false, reason: "mime_unsupported", rawBytes };
45
+ if (rawBytes > PDF_TARGET_RAW_SIZE)
46
+ return { ok: false, reason: "raw_too_large", rawBytes, cap: PDF_TARGET_RAW_SIZE };
47
+ if (!pdfMagicMatches(bytes))
48
+ return { ok: false, reason: "magic_mismatch", rawBytes };
49
+ return { ok: true, block: { type: "document", data: Buffer.from(bytes).toString("base64"), mimeType: claimedMime }, originalSize: rawBytes };
50
+ }
51
+ const mediaTypeEcho = (mime) => typeof mime === "string" && /^[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]{0,126}\/[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]{0,126}$/.test(mime) ? mime : undefined;
52
+ const ADMITTED_FOR = { image: ADMITTED_IMAGE_MIME_TYPES, document: ADMITTED_DOCUMENT_MIME_TYPES };
53
+ const REFUSAL_CLAUSE = {
54
+ undecodable: () => "it carried no base64 payload",
55
+ mime_unsupported: (r) => `${r.mimeType ?? "its media type"} is not a ${r.type} type the model accepts (${ADMITTED_FOR[r.type].join(", ")})`,
56
+ raw_too_large: (r) => `its ${r.bytes} bytes exceed the ${r.limit}-byte cap`,
57
+ magic_mismatch: (r) => `its ${r.bytes} bytes do not carry the ${r.mimeType} signature`,
58
+ base64_too_large: (r) => `its base64 encoding exceeds the ${r.limit}-character provider limit (${r.bytes} bytes)`,
59
+ };
60
+ const placeholderOf = (toolName, r) => ({
61
+ type: "text",
62
+ text: `[${r.type} from the ${JSON.stringify(toolName)} tool not shown: ${REFUSAL_CLAUSE[r.reason](r)} (media admission: ${r.reason})]`,
63
+ });
64
+ const claimsMedia = (block) => claimedMediaType(block) !== undefined;
65
+ async function admitBlock(block) {
66
+ const type = claimedMediaType(block);
67
+ if (type === undefined)
68
+ return { block };
69
+ const data = readMember(block, "data");
70
+ const mime = readMember(block, "mimeType");
71
+ const echo = mediaTypeEcho(mime);
72
+ const refusal = (reason, bytes, limit) => ({
73
+ refusal: { type, reason, bytes, ...(limit !== undefined ? { limit } : {}), ...(echo !== undefined ? { mimeType: echo } : {}) },
74
+ });
75
+ if (typeof data !== "string")
76
+ return refusal("undecodable", 0);
77
+ let a;
78
+ try {
79
+ const bytes = Buffer.from(data, "base64");
80
+ const claimed = typeof mime === "string" ? mime : "";
81
+ a = type === "image" ? await admitImageBytes(bytes, claimed, undefined) : admitDocumentBytes(bytes, claimed);
82
+ }
83
+ catch {
84
+ return refusal("undecodable", 0);
85
+ }
86
+ if (a.ok)
87
+ return { block: a.block };
88
+ if (a.reason === "raw_too_large")
89
+ return refusal(a.reason, a.rawBytes, a.cap);
90
+ if (a.reason === "base64_too_large")
91
+ return refusal(a.reason, a.rawBytes, MCP_IMAGE_MAX_BASE64);
92
+ return refusal(a.reason, a.rawBytes);
93
+ }
94
+ export function admitToolResultMedia(content, toolName) {
95
+ let blocks;
96
+ try {
97
+ if (!Array.isArray(content))
98
+ return undefined;
99
+ blocks = Array.from(content);
100
+ if (!blocks.some(claimsMedia))
101
+ return undefined;
102
+ }
103
+ catch {
104
+ return undefined;
105
+ }
106
+ return (async () => {
107
+ const refused = [];
108
+ const out = [];
109
+ for (const b of blocks) {
110
+ const r = await admitBlock(b);
111
+ if ("block" in r) {
112
+ out.push(r.block);
113
+ }
114
+ else {
115
+ refused.push(r.refusal);
116
+ out.push(placeholderOf(toolName, r.refusal));
117
+ }
118
+ }
119
+ return { content: out, refused };
120
+ })();
121
+ }
@@ -0,0 +1,2 @@
1
+ /** Is `p` the root itself or inside it, both read physically? */
2
+ export declare function isPhysicallyWithin(root: string, p: string): boolean;
@@ -0,0 +1,5 @@
1
+ import { physicalPathOfExisting } from "../../core/physical-path.js";
2
+ import { withinAnyRoot } from "./safety.js";
3
+ export function isPhysicallyWithin(root, p) {
4
+ return withinAnyRoot([physicalPathOfExisting(root)], physicalPathOfExisting(p));
5
+ }
@@ -40,9 +40,30 @@ declare function unestablishedDetails(): {
40
40
  /** The structured details a refusal or a mandated card carries: the deny card is the read faces' own (`violationDetails`,
41
41
  * safety.ts), the containment and the unestablished cards are minted above. */
42
42
  export type ShellReadRefusalDetails = NonNullable<ReturnType<typeof violationDetails>> | ReturnType<typeof containmentDetails> | ReturnType<typeof unestablishedDetails>;
43
- /** What the station answers for ONE call. */
44
- export interface ShellReadVerdict {
45
- readonly disposition: ShellReadDisposition;
43
+ /**
44
+ * What the station answers for ONE call: a VOUCH — the finding `none`, nothing beside it but the facts — or a QUESTION, a
45
+ * disposition the decision table gives its finding ({@link import("./shell-read-judge.js").dispositionsOfRow}). A vouch carries no
46
+ * finding, no silence mark and no card: the table vouches on the `none` row alone
47
+ * ({@link import("./shell-read-judge.js").VouchOnlyOnTheNoneRow}), so a vouch beside anything else is a contradiction — one this
48
+ * type cannot spell, and one every reader of a station's answer refuses where an untyped station spells it anyway
49
+ * ({@link import("../../core/shell-read-silence.js").readStationAnswer}).
50
+ */
51
+ export type ShellReadVerdict = ShellReadVouch | ShellReadQuestion;
52
+ /** The station has no question about the call. */
53
+ export interface ShellReadVouch extends ShellReadVerdictFacts {
54
+ readonly disposition: "vouch";
55
+ readonly finding: "none";
56
+ readonly environmentSilent?: never;
57
+ readonly rowsUnread?: never;
58
+ readonly face?: never;
59
+ readonly sentence?: never;
60
+ readonly details?: never;
61
+ readonly readRootCandidate?: never;
62
+ readonly unestablished?: never;
63
+ }
64
+ /** The station refuses the call or puts a question about it. */
65
+ export interface ShellReadQuestion extends ShellReadVerdictFacts {
66
+ readonly disposition: Exclude<ShellReadDisposition, "vouch">;
46
67
  readonly finding: ShellReadFinding;
47
68
  /** The environment did not answer the judgment — a resolver, a listing or an identity lookup gave no answer, a bound was
48
69
  * reached, the judge itself did not answer: present on the `unanswered` finding the fold or the fallback gives. The gate
@@ -66,6 +87,13 @@ export interface ShellReadVerdict {
66
87
  readonly details?: ShellReadRefusalDetails;
67
88
  /** `mandated` on an `outside_roots` finding: the directory whose grant would clear this ask. */
68
89
  readonly readRootCandidate?: ReadRootGrantCandidate;
90
+ /** A FACT beside the disposition: some read of the command could not be established (an `unestablishable` or `declined`
91
+ * finding anywhere in it, whichever finding won). A mode that releases the call releases it past the read-deny table. */
92
+ readonly unestablished?: true;
93
+ }
94
+ /** The FACTS a station's answer may carry beside either disposition — about what the command writes, read by the modes that
95
+ * answer file commands and by the write classes, never by the read table — and the admission the applying reader activates. */
96
+ export interface ShellReadVerdictFacts {
69
97
  /** A FACT beside the disposition, for the mode that answers a file command itself: every write target this command
70
98
  * names was established inside the boundary's WRITE roots at every seat the program writes it (the entry, where it
71
99
  * lands, or both — `WriteSeat`, shell-read-write-targets.ts), off the write-protection table and off every deployment
@@ -91,9 +119,6 @@ export interface ShellReadVerdict {
91
119
  readonly unplacedOptions?: {
92
120
  readonly program: "cp" | "mv";
93
121
  };
94
- /** A FACT beside the disposition: some read of the command could not be established (an `unestablishable` or `declined`
95
- * finding anywhere in it, whichever finding won). A mode that releases the call releases it past the read-deny table. */
96
- readonly unestablished?: true;
97
122
  /** The station's admission of this call's judged target, activated only by the caller that APPLIES this verdict (the
98
123
  * gate's fold, the resumed leg's belt): a verdict nobody received — a wait that elapsed, a judgment that threw —
99
124
  * admits nothing, whenever it completes. */
@@ -288,7 +288,7 @@ function verdictOf(tool, seat, fold, judged, boundary) {
288
288
  const redirectsOnly = fold.finding === "outside_roots" && fold.unestablished !== true && outsideFindingsAreRedirections(judged);
289
289
  const disposition = decideShellRead(fold.finding, seat, { redirectsOnly, narrowedReadFace: fold.unplacedRead === true && boundary.face !== "open" });
290
290
  if (disposition === "vouch")
291
- return { disposition, finding: fold.finding };
291
+ return { disposition, finding: "none" };
292
292
  const paths = outsidePathsOf(judged);
293
293
  const unestablished = fold.unestablished === true ? { unestablished: true } : {};
294
294
  const face = faceOf(fold);
@@ -354,14 +354,12 @@ async function runShellReadStation(tool, command, declared, seat, readers, estab
354
354
  const boundary = { ...declared, roots: established.roots };
355
355
  const onPlan = await applyAlsoInside(judgePlan(plan, established, boundary), boundary);
356
356
  const judgedNow = () => {
357
- const unfinished = door.settled();
357
+ const unfinished = door.incomplete();
358
358
  return unfinished.length > 0 ? { ...onPlan, incomplete: unfinished } : onPlan;
359
359
  };
360
360
  let judged = judgedNow();
361
361
  let judgedVerdict = verdictOf(tool, seat, foldShellReadFindings(judged), judged, boundary);
362
- const probesFrom = door.mark();
363
362
  const guarded = judgedVerdict.disposition === "refuse" ? { unplaced: false, writeProtected: false } : await writeGuardsOf(judged, boundary, door.fs, signal);
364
- door.silence(probesFrom);
365
363
  judged = judgedNow();
366
364
  judgedVerdict = verdictOf(tool, seat, foldShellReadFindings(judged), judged, boundary);
367
365
  const unplaced = unplacedOptionsOf(judged);
@@ -10,15 +10,10 @@
10
10
  * cannot escape the record.
11
11
  *
12
12
  * What is NOT a failure: a conclusive absence (`not_found`, `not_directory` — the answer is "nothing there") and an adapter
13
- * that declares no filesystem surface (`not_supported` — it keeps the lexical reading). Everything else is.
14
- *
15
- * ONE decision, taken once per command: a resolver that did not declare its answers authoritative and answered NOTHING AT ALL,
16
- * in the whole command, is read as an absence (the lexical reading every such adapter has had). It is a statement about the
17
- * command, not about a word or a station: the walks and probes that found the resolver silent are marked
18
- * ({@link ObservedEnvironment.silence}), and {@link ObservedEnvironment.settled} — read by whoever made the door, after EVERY
19
- * lookup the command makes (the establishment, the write guards) has asked — takes the marked records back only when no lookup
20
- * was ever answered. A resolver that answered once has shown it can; a silence after that is a failure, wherever it falls. A
21
- * resolver that declared its answers authoritative is never read as an absence.
13
+ * that has no filesystem surface (`not_supported` — its answer that there is no file face: a chain it answers so keeps the
14
+ * lexical reading). Everything else is, and is recorded wherever it falls — a resolver that answered nothing at all in the
15
+ * command included: a lookup that failed (`permission_denied`, `timeout`, `unknown`, a throw) is a resolver failing, whose
16
+ * identity of the words cannot be judged, never an adapter without a file face.
22
17
  */
23
18
  import type { ExecutionEnv } from "../../internal/harness-types.js";
24
19
  /** One thing the station could not finish: an environment lookup that gave no answer, or a bound the station stopped at. */
@@ -42,24 +37,15 @@ export type ObservedFilesystem = ExecutionEnv & {
42
37
  /** The observed door for one command. */
43
38
  export interface ObservedEnvironment {
44
39
  readonly fs: ObservedFilesystem;
45
- /** Whether the environment DECLARES its `canonicalPath` the authority on where a path lands: its silence is then a failure
46
- * that nothing takes back ({@link silence} does nothing). */
40
+ /** Whether the environment DECLARES its `canonicalPath` the authority on where a path lands: a chain it does not answer at all
41
+ * is then not placed (the establishment's reading; the door records the lookups alike either way). */
47
42
  readonly authoritative: boolean;
48
- /** What the door recorded, in the order it happened, as the command stands on it NOW — the one decision about what a
49
- * silence means, taken on everything asked so far; call it after the last lookup of the command. */
50
- settled(): Incomplete[];
43
+ /** What the door recorded, in the order it happened; call it after the last lookup of the command. */
44
+ incomplete(): Incomplete[];
51
45
  /** The station stopped at its own bound (more directories or names than it enumerates, a link chain past its hop bound). */
52
46
  bound(detail: string): void;
53
- /** Records that came from outside the door and stand as they are: the declared roots' own lookup (`answered`: it was
54
- * answered, which counts as the resolver's answer) and the home views a read could not be placed against. */
55
- adopt(records: readonly Incomplete[], answered?: boolean): void;
56
- /** Whether the resolver answered a path question (a path, or a conclusive "nothing there") at least once. */
57
- answered(): boolean;
58
- /** Where a walk's (or a probe's) records start: pass it to {@link silence} once the lookups are done. */
59
- mark(): number;
60
- /** The lookups since `from` found the resolver silent: their records are taken back by {@link settled} only when the
61
- * resolver never answered anything in the command (and never where it is {@link authoritative}). */
62
- silence(from: number): void;
47
+ /** Records that came from outside the door: the declared roots' own lookup and the home views a read could not be placed against. */
48
+ adopt(records: readonly Incomplete[]): void;
63
49
  }
64
50
  /** The declared roots, canonicalized ONCE per establisher (through the same door), with what that lookup could not finish —
65
51
  * merged into every establishment. `known`: the caller already holds them as canonical identities and they are used verbatim
@@ -67,7 +53,6 @@ export interface ObservedEnvironment {
67
53
  export declare function canonicalRootsOnce(env: ExecutionEnv, readRoots: readonly string[], known: boolean): (signal?: AbortSignal) => Promise<{
68
54
  readonly roots: readonly string[];
69
55
  readonly incomplete: readonly Incomplete[];
70
- readonly answered: boolean;
71
56
  }>;
72
57
  /** Wrap `env` so every lookup that fails inconclusively is recorded; the result a caller sees is the environment's own. */
73
58
  export declare function observeEnvironment(env: ExecutionEnv): ObservedEnvironment;
@@ -1,9 +1,8 @@
1
1
  import { canonicalPathIsAuthoritative } from "../../core/remote-env.js";
2
2
  const OBSERVED = Symbol("observedFilesystem");
3
3
  const CONCLUSIVE = new Set(["not_found", "not_directory", "not_supported"]);
4
- const ABSENCES = new Set(["not_found", "not_directory"]);
5
4
  export function canonicalRootsOnce(env, readRoots, known) {
6
- let once = known ? Promise.resolve({ roots: readRoots, incomplete: [], answered: false }) : undefined;
5
+ let once = known ? Promise.resolve({ roots: readRoots, incomplete: [] }) : undefined;
7
6
  return (signal) => (once ??= (async () => {
8
7
  const seen = observeEnvironment(env);
9
8
  const roots = [];
@@ -11,7 +10,7 @@ export function canonicalRootsOnce(env, readRoots, known) {
11
10
  const c = await seen.fs.canonicalPath(r, signal);
12
11
  roots.push(c.ok ? c.value : r);
13
12
  }
14
- const result = { roots, incomplete: seen.settled(), answered: seen.answered() };
13
+ const result = { roots, incomplete: seen.incomplete() };
15
14
  if (result.incomplete.length > 0)
16
15
  once = undefined;
17
16
  return result;
@@ -20,11 +19,8 @@ export function canonicalRootsOnce(env, readRoots, known) {
20
19
  export function observeEnvironment(env) {
21
20
  const authoritative = canonicalPathIsAuthoritative(env);
22
21
  const records = [];
23
- let answers = 0;
24
- const note = (op, path, what, code) => void records.push({ op, detail: `"${path}" ${what} (${code})`, silent: false });
22
+ const note = (op, path, what, code) => void records.push({ op, detail: `"${path}" ${what} (${code})` });
25
23
  const watch = (op, path, what, ask) => new Promise((resolve) => resolve(ask())).then((r) => {
26
- if (op === "canonicalPath" && (r.ok || ABSENCES.has(r.error.code)))
27
- answers += 1;
28
24
  if (!r.ok && !CONCLUSIVE.has(r.error.code))
29
25
  note(op, path, what, r.error.code);
30
26
  return r;
@@ -56,16 +52,8 @@ export function observeEnvironment(env) {
56
52
  return {
57
53
  fs,
58
54
  authoritative,
59
- settled: () => records.filter((r) => answers > 0 || !r.silent).map(({ op, detail }) => ({ op, detail })),
60
- bound: (detail) => void records.push({ op: "bound", detail, silent: false }),
61
- adopt: (adopted, answered = false) => {
62
- for (const r of adopted)
63
- records.push({ ...r, silent: false });
64
- if (answered)
65
- answers += 1;
66
- },
67
- mark: () => records.length,
68
- silence: (from) => (authoritative ? undefined : void records.slice(from).forEach((r) => void (r.silent = true))),
69
- answered: () => answers > 0,
55
+ incomplete: () => records.map(({ op, detail }) => ({ op, detail })),
56
+ bound: (detail) => void records.push({ op: "bound", detail }),
57
+ adopt: (adopted) => void records.push(...adopted.map(({ op, detail }) => ({ op, detail }))),
70
58
  };
71
59
  }
@@ -17,8 +17,9 @@
17
17
  * target, so the target is established where the link lands (read one hop at a time, bounded) — or not at
18
18
  * all when the environment cannot read the link.
19
19
  * 2. An unanswerable filesystem manufactures no refusal on its own: a chain no ancestor answered degrades
20
- * to the lexical spelling on an environment that declares no authority for its resolver AND answered nothing
21
- * in the whole command, and is `unestablishable` on one that does (`canonicalPathIsAuthoritative`).
20
+ * to the lexical spelling on an environment that declares no authority for its resolver, and is `unestablishable` on
21
+ * one that does (`canonicalPathIsAuthoritative`); only `not_supported` (no file face) is no failure — any other non-answer
22
+ * is recorded at the door and the command is `unanswered` (shell-read-environment.ts).
22
23
  * 3. An ISOLATED environment stays lexical, disclosed rather than silently degraded: the sandbox is the
23
24
  * containment there and `canonicalPath` is an RPC into a filesystem that is not this host's
24
25
  * (`boundaryIsTheSandbox`). The split is the DECLARED isolation capability, never the remote interface's
@@ -174,7 +174,8 @@ function writtenByText(plan, boundary) {
174
174
  }
175
175
  function unfollowedBackupReads(j, boundary, cause) {
176
176
  const spellings = sedBackupNames(j.followed.operand.spelled, j.followed.suffix).flatMap((name) => lexicalReadsOf({ ...j.candidate, spelled: name }, boundary).flatMap((r) => [...(r.target !== undefined ? [r.target] : []), ...r.spellings]));
177
- return [{ candidate: j.candidate, established: "unestablishable", spellings: [...new Set(spellings)], cause }];
177
+ const read = { candidate: j.candidate, established: "unestablishable", spellings: [...new Set(spellings)], cause };
178
+ return (j.seats ?? ["landing"]).map((seat) => writtenAt(read, j, seat));
178
179
  }
179
180
  function writtenAt(read, j, seat) {
180
181
  if (read.candidate.face !== "write" || !(j.written ?? j.seats ?? ["landing"]).includes(seat))
@@ -205,8 +206,8 @@ export function createShellReadEstablisher(env, readRoots, opts) {
205
206
  establish: async (plan, boundary, signal, door = observeEnvironment(env)) => {
206
207
  if (lexicalOnly)
207
208
  return establishShellReadsLexically(plan, boundary);
208
- const { roots, incomplete, answered } = await canonicalRoots(signal);
209
- door.adopt(incomplete, answered);
209
+ const { roots, incomplete } = await canonicalRoots(signal);
210
+ door.adopt(incomplete);
210
211
  return await withHomeViews(door, opts, await legOver(door)(plan, { ...boundary, roots }, roots, signal), signal);
211
212
  },
212
213
  reestablish: async (previous, boundary, signal) => {
@@ -229,7 +230,6 @@ function establishLegOver(seen, readRoots) {
229
230
  const parts = p.split(SEP);
230
231
  const tail = [];
231
232
  let allAbsent = true;
232
- const began = seen.mark();
233
233
  while (parts.length > 0) {
234
234
  const at = parts.join("/") || "/";
235
235
  const canon = await env.canonicalPath(at, signal);
@@ -252,7 +252,7 @@ function establishLegOver(seen, readRoots) {
252
252
  allAbsent = false;
253
253
  tail.push(parts.pop());
254
254
  }
255
- return void seen.silence(began);
255
+ return undefined;
256
256
  };
257
257
  const parentOf = (canonical) => normalizeAbsPathLexically(`${canonical}/..`);
258
258
  const resolve = async (p, signal, writeTarget = false, linkHops = WRITE_LINK_HOPS) => {
@@ -52,14 +52,18 @@ export type RankTableCoversEveryFinding = AssertAllKeysHandled<Exclude<ShellRead
52
52
  * the auto-mode classifier, a stored allow rule) but no answerless engine table may; `vouch` = the station
53
53
  * has no question. */
54
54
  export type ShellReadDisposition = "refuse" | "mandated" | "ordinary" | "vouch";
55
- /** One cell of the decision table: a disposition, or — where the READ FACE the question was raised under decides it — the two
56
- * readings, `narrowed` (the `roots` face: the fence the deployment narrowed, and the engine default) and `open`. */
55
+ /** One cell of the decision table: a disposition, or — where the FORM of the question decides it — its readings: `narrowed` (raised
56
+ * under the `roots` read face: the fence the deployment narrowed, and the engine default), `open` (under the open read face, or a
57
+ * written target alone) and, where it differs, `redirect` (every containment finding is an output redirection outside the write
58
+ * roots). */
57
59
  export type ShellReadCell = ShellReadDisposition | {
58
60
  readonly narrowed: ShellReadDisposition;
59
61
  readonly open: ShellReadDisposition;
62
+ readonly redirect?: ShellReadDisposition;
60
63
  };
61
- /** A cell, read for a question raised under a narrowed read face (`true`) or an open one (`false`). */
62
- export declare function dispositionOfCell(cell: ShellReadCell, narrowedReadFace: boolean): ShellReadDisposition;
64
+ /** A cell, read for the form of the question ({@link ShellReadCellContext}): the redirect reading where the cell has one and every
65
+ * containment finding is a redirection, otherwise the read face's (absent ⇒ narrowed, the engine default). */
66
+ export declare function dispositionOfCell(cell: ShellReadCell, context?: ShellReadCellContext): ShellReadDisposition;
63
67
  /**
64
68
  * THE decision table — total over findings × seats; no cell is n/a. Row by row:
65
69
  * · `deny_hit` — refused on every seat, for a spelling the station can establish: a deny row is a floor
@@ -67,7 +71,13 @@ export declare function dispositionOfCell(cell: ShellReadCell, narrowedReadFace:
67
71
  * the `unestablishable` row) — the floor holds what it can place, and says so rather than guessing.
68
72
  * · `outside_roots` — asked where a person can be asked (the full shell), refused where none can (the
69
73
  * read-only face has no approval channel; a rewrite that leaves the roots after approval was seen by
70
- * nobody).
74
+ * nobody). The full shell's ask is MANDATED, except for a command whose every containment finding is an OUTPUT
75
+ * REDIRECTION outside the write roots (the cell's `redirect` reading): the permission question a stored allow answers,
76
+ * the ORDINARY ask. The reference implementation's redirection check (CC 2.1.296 `lvr` (cli296.js:249645) @18502854,
77
+ * called @18508831) asks such a target as a plain permission ask that is not rule-overridable: a whole-tool `Bash` allow
78
+ * (`$pn` @18717070) and bypass (`$pn` @18716980) release it, a prefix allow does not (`vX`'s ask returns before the allow
79
+ * rules, @18567915). A command that also reads outside the roots, or writes a program operand there (`tee`, `cp`, …),
80
+ * keeps the mandate.
71
81
  * · `unestablishable` — the floor could not say where the command reads, so it is not vouched anywhere: asked
72
82
  * where a person can be asked (the full shell), refused where none can be asked. The full shell's cell is read by the read face
73
83
  * the question was raised under: on a NARROWED (`roots`) face — the engine default — the deployment asked to be asked, so the
@@ -101,7 +111,11 @@ export declare const SHELL_READ_DECISION: {
101
111
  readonly rejudge: "refuse";
102
112
  };
103
113
  readonly outside_roots: {
104
- readonly full_shell: "mandated";
114
+ readonly full_shell: {
115
+ readonly narrowed: "mandated";
116
+ readonly open: "mandated";
117
+ readonly redirect: "ordinary";
118
+ };
105
119
  readonly readonly: "refuse";
106
120
  readonly rejudge: "refuse";
107
121
  };
@@ -142,15 +156,30 @@ export type DecisionTableCoversEverySeat = AssertAllKeysHandled<Exclude<ShellRea
142
156
  /** The disposition of one command-level finding on one seat — the table, read. A seat word outside
143
157
  * {@link SHELL_READ_SEATS} (an untyped caller, or one still passing a retired word) is refused loudly: an
144
158
  * absent disposition would read as "no verdict", which no caller can act on safely. */
145
- export declare function decideShellRead(finding: ShellReadFinding, seat: ShellReadSeat, context?: ShellReadCellContext): ShellReadDisposition;
159
+ export declare function decideShellRead<F extends ShellReadFinding>(finding: F, seat: ShellReadSeat, context?: ShellReadCellContext): RowDisposition<F>;
160
+ /** The dispositions the table's row for `F` holds — on any seat, under any form of the question (the table's own literal words). */
161
+ export type RowDisposition<F extends ShellReadFinding> = CellWords<(typeof SHELL_READ_DECISION)[F][ShellReadSeat]>;
146
162
  /** What the table is read for beyond (finding, seat): the two facts of a judged command a cell can depend on. */
147
163
  export interface ShellReadCellContext {
148
- /** Every containment finding is an output redirection outside the write roots ({@link SHELL_REDIRECT_DECISION}). Absent ⇒ no. */
164
+ /** Every containment finding is an output redirection outside the write roots (a cell's `redirect` reading). Absent ⇒ no. */
149
165
  readonly redirectsOnly?: boolean;
150
166
  /** The question was raised under a NARROWED (`roots`) read face — a read candidate or the text's own structure while the read
151
167
  * face runs the roots containment; `false` for the open face and for a written target alone. Absent ⇒ narrowed, the engine default. */
152
168
  readonly narrowedReadFace?: boolean;
153
169
  }
170
+ /** Every disposition the table gives `finding` — on any seat, under any form of the question; empty for a word outside
171
+ * {@link SHELL_READ_FINDINGS}. A disposition outside it is no reading of the table: what a reader of a station's answer
172
+ * checks the answer against. */
173
+ export declare function dispositionsOfRow(finding: string): ReadonlySet<ShellReadDisposition>;
174
+ /** The words a cell can read as. */
175
+ type CellWords<C> = C extends string ? C : C[keyof C];
176
+ /** The findings whose row the table vouches on, on any seat and under any form. */
177
+ type FindingsTheTableVouches = {
178
+ [F in ShellReadFinding]: "vouch" extends RowDisposition<F> ? F : never;
179
+ }[ShellReadFinding];
180
+ /** Compile-time fence: `never` while `vouch` is a cell of the `none` row alone — what lets a vouch carry the finding `none` and
181
+ * nothing else ({@link import("./shell-read-check.js").ShellReadVerdict}). */
182
+ export type VouchOnlyOnTheNoneRow = AssertAllKeysHandled<Exclude<FindingsTheTableVouches, "none">>;
154
183
  /**
155
184
  * The boundary a shell read is judged against: the declared roots (canonical), the working directory and
156
185
  * home the plan's bases start from, the deny seat (the SAME `denyMatch` closure every read face mounts),
@@ -399,3 +428,4 @@ export declare function grantOfferFor(candidate: string, single: string | undefi
399
428
  } | undefined;
400
429
  /** The same judged plan under another boundary — the established reads are kept, only the judgment reruns. */
401
430
  export declare function rejudge(judged: JudgedPlan, boundary: ShellReadBoundary): JudgedPlan;
431
+ export {};