@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
@@ -14,26 +14,37 @@
14
14
  * (mirrors `steer.trusted`); core never adjudicates operator-ness, it only honors the flag.
15
15
  * - **CAS rev** (optimistic concurrency): a `putRules` with a stale `expectedRev` is rejected, so two replicas
16
16
  * can't lose an update (mirrors the checkpoint store's `resolve` OCC).
17
- * - **structured fields only**: core interprets `toolAllow/Deny`, `allowDirs`, `commandAllow/Deny` (argv[0]
18
- * names). A richer rule DSL is the service's/profile's job — core grows no rule interpreter.
19
- * - **NOT A SANDBOX**: command rules inherit the coarse argv[0] matching (wrappers like `sh -c`/`sudo` circumvention);
20
- * the real isolation boundary is the executionEnv, never these rules.
17
+ * - **one grammar**: the two rule buckets hold rule ENTRIES — the grammar every entry list reads, through its one reader
18
+ * (`readRuleEntry`, allow-entry.ts): a tool name, a protocol namespace, a shell command rule (`Bash(git push:*)`), a
19
+ * path rule, a name or host rule. Entries are stored as written; every station that validates, compares, matches or
20
+ * names one reads that reader's answer ({@link judgeSessionRulesWrite}, {@link loosenReasons}, the run-time layer).
21
+ * - **NOT A SANDBOX**: a shell command rule reads the command line the call carries (every program run in its connector
22
+ * chain); the real isolation boundary is the executionEnv, never these rules.
21
23
  */
22
- import { type RuleToolNameVerdict } from "./tool-name-aliases.js";
23
- /** The structured per-session rules. All optional; an absent field imposes no constraint of that kind. */
24
+ import { type AllowEntryInertDetail, type AllowEntryInertReason } from "./allow-entry.js";
25
+ /** The per-session rules. All optional; an absent bucket imposes no constraint of that kind. */
24
26
  export interface SessionPermissionRules {
25
- /** If set, ONLY these tool names are allowed (others denied). A narrowing allowlist. */
27
+ /** If set, ONLY the calls these entries admit are allowed — a narrowing allowlist: a call no entry admits is refused.
28
+ * Rule entries read in the allow direction (`Read`, `mcp__srv`, `Bash(git:*)`, `Edit(src/**)`). */
26
29
  toolAllow?: string[];
27
- /** Tool names always denied (deny wins). */
30
+ /** Entries whose calls are always refused (deny wins) — rule entries read in the deny direction (`Bash`, `Bash(rm:*)`,
31
+ * `mcp__srv__delete_*`, `Read(//etc/**)`). */
28
32
  toolDeny?: string[];
29
33
  /** If set, WRITE tools (`Write`/`Edit`) may only target paths within these dirs; a write-capable
30
34
  * tool that can't be path-confined (e.g. `Bash`) is denied while this is set. Stored RAW; resolved at run time. */
31
35
  allowDirs?: string[];
32
- /** If set, ONLY these `Bash` command names (argv[0]) are allowed. */
33
- commandAllow?: string[];
34
- /** `Bash` command names (argv[0]) always denied. */
35
- commandDeny?: string[];
36
36
  }
37
+ /** Every bucket a rules object holds, and how its entries are read: a rule-entry list in one direction, or the write
38
+ * confinement's directories. A key outside this table is no bucket ({@link readSessionRulesShape} refuses it). */
39
+ declare const SESSION_RULE_BUCKETS: {
40
+ readonly toolAllow: "allow";
41
+ readonly toolDeny: "deny";
42
+ readonly allowDirs: "dirs";
43
+ };
44
+ /** A bucket that holds rule entries. */
45
+ export type SessionRuleEntryField = {
46
+ [K in keyof typeof SESSION_RULE_BUCKETS]: (typeof SESSION_RULE_BUCKETS)[K] extends "dirs" ? never : K;
47
+ }[keyof typeof SESSION_RULE_BUCKETS];
37
48
  /** {@link SessionPermissionRules} plus the monotonic `rev` the store stamps (the OCC key). */
38
49
  export interface StoredSessionRules extends SessionPermissionRules {
39
50
  rev: number;
@@ -52,58 +63,42 @@ export interface PutRulesOptions {
52
63
  * this from a verified, role-checked principal (mirrors `steer.trusted`). */
53
64
  operator?: boolean;
54
65
  }
55
- /** The {@link SessionPolicyError.detail} of a tool-name refusal: which bucket, which entry, the judgment's word, the judgment's
56
- * one-line sentence when it has one, and the spelling to write instead when the judgment has one (a retired name's 1:1
57
- * successor, an unmintable segment's minted name). */
58
- export interface SessionRuleNameRefusal {
59
- field: SessionRuleToolNameField;
60
- name: string;
61
- verdict: Exclude<RuleToolNameVerdict["kind"], "current">;
62
- /** The `guidance` of the judgment ({@link judgeRuleToolName}), byte for byte — the same string the refusal's sentence quotes. Present for
63
- * `retired` and `unmintable_segment`, the two verdicts that carry one; absent for `unprefixed_namespace`, whose judgment has no sentence of its own. */
64
- guidance?: string;
65
- currentName?: string;
66
+ /** The {@link SessionPolicyError.detail} of an `invalid_shape` refusal: which key, and what it holds — its TYPE, never its
67
+ * value (`a string`, `null`, `an array holding an entry that is not a string`), or `not a rule bucket` for a key outside
68
+ * the buckets. `field` is empty when the rules value itself is not an object. */
69
+ export interface SessionRuleShapeRefusal {
70
+ field: string;
71
+ got: string;
72
+ }
73
+ /** The {@link SessionPolicyError.detail} of an `inert_entry` refusal: which bucket, which entry, and why it reads as nothing —
74
+ * the entry reader's own answer (`readRuleEntry`), reason and facts unchanged. */
75
+ export interface SessionRuleEntryRefusal extends AllowEntryInertDetail {
76
+ field: SessionRuleEntryField;
77
+ entry: string;
78
+ reason: AllowEntryInertReason;
66
79
  }
67
80
  /** Typed error from {@link SessionPolicyStore.putRules}. `conflict` = stale `expectedRev`; `loosen_forbidden`
68
- * = a non-operator write tried to relax the rules; `corrupt` = a backend could not read the erase-surviving
69
- * rev mark ({@link SessionPolicyStore.deleteBySession}) and refuses rather than stamp an unproven rev; the fourth
70
- * code = a tool-name bucket names a retired name, a prefix-less `__` name or a segment no mint spells (an empty one, or a peer spelled otherwise)
71
- * ({@link assertSessionRuleNamesCurrent}) — `detail` is present on that code and only on it. */
81
+ * = a non-operator write tried to relax the rules; `corrupt` = a backend could not read what it stored (a row, or the
82
+ * erase-surviving rev mark of {@link SessionPolicyStore.deleteBySession}) and refuses rather than read it as fewer rules or
83
+ * stamp an unproven rev; `invalid_shape` = the rules value is not an object of buckets, each a list of strings
84
+ * ({@link readSessionRulesShape}); `inert_entry` = a bucket holds an entry the entry reader reads as nothing. `detail` is
85
+ * present on the last two codes and only on them ({@link SessionRuleShapeRefusal}, {@link SessionRuleEntryRefusal}). */
72
86
  export declare class SessionPolicyError extends Error {
73
- readonly code: "conflict" | "loosen_forbidden" | "corrupt" | "legacy_tool_name";
74
- readonly detail?: SessionRuleNameRefusal | undefined;
75
- constructor(code: "conflict" | "loosen_forbidden" | "corrupt" | "legacy_tool_name", message: string, detail?: SessionRuleNameRefusal | undefined);
87
+ readonly code: "conflict" | "loosen_forbidden" | "corrupt" | "invalid_shape" | "inert_entry";
88
+ readonly detail?: (SessionRuleShapeRefusal | SessionRuleEntryRefusal) | undefined;
89
+ constructor(code: "conflict" | "loosen_forbidden" | "corrupt" | "invalid_shape" | "inert_entry", message: string, detail?: (SessionRuleShapeRefusal | SessionRuleEntryRefusal) | undefined);
76
90
  }
77
- /** Which buckets hold TOOL names (the ones a run's policy audit reads as names); a new bucket is given a role here. */
78
- declare const HOLDS_TOOL_NAMES: {
79
- readonly toolAllow: true;
80
- readonly toolDeny: true;
81
- readonly allowDirs: false;
82
- readonly commandAllow: false;
83
- readonly commandDeny: false;
84
- };
85
- type SessionRuleToolNameField = {
86
- [K in keyof typeof HOLDS_TOOL_NAMES]: (typeof HOLDS_TOOL_NAMES)[K] extends true ? K : never;
87
- }[keyof typeof HOLDS_TOOL_NAMES];
88
91
  /** Construction options both bundled stores take. */
89
92
  export interface SessionPolicyStoreOptions {
90
- /** The names this deployment mounts its OWN tools under — the roster the store judges a rule's tool names against
91
- * ({@link judgeRuleToolName}): a retired name declared here is an ordinary name to the write and to the loosening
92
- * check alike. Absent = none. A name carrying `__` is refused, as it is for a deployment tool. */
93
+ /** The names this deployment mounts its OWN tools under — the roster the store reads a rule entry's tool name against
94
+ * (`readRuleEntry`): a retired name declared here is an ordinary name to the write and to the loosening check alike.
95
+ * Absent = none. A name carrying `__` is refused, as it is for a deployment tool. */
93
96
  mountedToolNames?: Iterable<string>;
94
97
  }
95
98
  /** A store's declared roster from {@link SessionPolicyStoreOptions.mountedToolNames} — the one reader both bundled stores
96
99
  * (and so `FileStorageBackendOptions.sessionPolicy`) construct through, refusing loudly when it is not a list of plain
97
100
  * tool names (a bare string would iterate by character). */
98
101
  export declare function readMountedToolNames(value: unknown): ReadonlySet<string>;
99
- /**
100
- * The write judge for session rules, against `mountedToolNames` (the store's declaration; empty when it has none): every
101
- * entry of a tool-name bucket must be `current` ({@link judgeRuleToolName}), or the write is refused (the fourth
102
- * {@link SessionPolicyError} code) — such a name matches no tool, and a run's policy audit refuses to start with it.
103
- * Operator writes included. Both bundled stores call it after normalizing and before {@link loosenReasons}; a durable
104
- * backend calls it at the same point. The message names the entry and the way out, never a storage location.
105
- */
106
- export declare function assertSessionRuleNamesCurrent(rules: SessionPermissionRules, mountedToolNames?: ReadonlySet<string>): void;
107
102
  /**
108
103
  * The seam a deployment implements for DURABLE, cross-replica per-session rules. Unset on `RunnerDeps` ⇒ the
109
104
  * feature is OFF (no rules read, zero behavior change). Keyed on `(sessionId, principal-owner)`: core passes
@@ -118,11 +113,9 @@ export interface SessionRulesRecord {
118
113
  }
119
114
  export interface SessionPolicyStore {
120
115
  getRules(sessionId: string, principal?: string): Promise<StoredSessionRules | null>;
121
- /** Write the rules for a key: CAS on `expectedRev`, then the name judge ({@link assertSessionRuleNamesCurrent} —
122
- * operator writes included), then, for a non-operator write over an existing row, the
123
- * tighten-only check ({@link loosenReasons}) — both against the names the deployment declares it mounts its own
124
- * tools under. A bucket that is not a list of strings (a bare `"rm"` for `commandDeny`) throws a `TypeError` from
125
- * {@link normalizeRules} before any of those is judged, and nothing is written. */
116
+ /** Write the rules for a key through the one write judge ({@link judgeSessionRulesWrite}): CAS on `expectedRev`, the
117
+ * shape, every entry's reading (operator writes included), then, for a non-operator write over an existing row, the
118
+ * tighten-only check — each refusal a {@link SessionPolicyError}, and nothing is written. */
126
119
  putRules(sessionId: string, principal: string | undefined, rules: SessionPermissionRules, opts?: PutRulesOptions): Promise<StoredSessionRules>;
127
120
  /**
128
121
  * 2c session-sync: every (principal, rules) record for `sessionId` across ALL principals — `getRules`
@@ -172,21 +165,47 @@ export interface SessionPolicyStore {
172
165
  * backend should keep `allowDirs` to real directories; a future symlink-aware write-time canonicalize closes it.
173
166
  */
174
167
  declare function lexWithin(root: string, p: string): boolean;
175
- /** Normalize a rules object's `allowDirs` (lexical `posix.normalize`) so stored rules are `..`-free and the
176
- * tighten diff compares the same canonical-ish form the runtime resolves. Every bucket must be a list of strings
177
- * ({@link ruleList}); a bucket that is not one throws `TypeError`, so neither bundled store (nor a durable twin that
178
- * normalizes through this function) can store a rule list the matcher would read differently from the one written. */
179
- export declare function normalizeRules(rules: SessionPermissionRules): SessionPermissionRules;
180
168
  /**
181
- * Pure tighten-only diff: the reasons `next` LOOSENS `prior` (empty ⇒ `next` is a pure tightening or no-op).
182
- * Direction per field — deny-lists: removing an entry loosens, except a `toolDeny` entry that is not `current` against
183
- * `mountedToolNames` ({@link judgeRuleToolName}: it matches no tool, so removing it relaxes nothing — the way a session's owner
184
- * takes out a stale entry that stops every run). allowlists: removing the list (→ everything allowed) or adding an
185
- * entry outside the prior set loosens; prior-undefined → any defined next only narrows. allowDirs: removing
186
- * confinement, or adding a dir not within some prior dir, loosens (coarse raw containment). `mountedToolNames` = the
187
- * store's declaration ({@link SessionPolicyStoreOptions.mountedToolNames}; empty when it has none) — the write judge's too.
169
+ * THE ONE SHAPE READING of a rules value — what every station that takes one reads it through: the write (incoming rules),
170
+ * a store's read-back of its own rows, the tighten comparison's prior, the run-time layer's served row. A rules value is an
171
+ * object whose keys are buckets ({@link SessionPermissionRules}); each bucket present is a list of strings (`allowDirs`: of
172
+ * non-blank strings — a directory is no blank). A bucket's TYPE is all this reads: what an entry means is the entry reader's.
173
+ * The store's own stamps (`rev`, `gen`) are passed over, and a key holding `undefined` holds nothing. Anything else is a fault,
174
+ * named by key and by TYPE, never by value — a bare string is never one rule and never its characters (`"rm"` spread into
175
+ * `["r","m"]`), and a key outside the buckets is never dropped (a dropped deny is a deny nobody enforces).
176
+ */
177
+ export declare function readSessionRulesShape(value: unknown): {
178
+ rules: SessionPermissionRules;
179
+ } | {
180
+ fault: SessionRuleShapeRefusal;
181
+ sentence: string;
182
+ };
183
+ /** The CANONICAL stored form of a rules value: its shape read ({@link readSessionRulesShape} — a fault refuses with
184
+ * `invalid_shape`), every list copied (detached from the caller's arrays), and `allowDirs` lexically `posix.normalize`d so the
185
+ * stored form is `..`-free and the tighten diff compares the form the runtime resolves. Both bundled stores and a durable twin
186
+ * stamp exactly this, so a structuredClone and a JSON round-trip of it are byte-identical. */
187
+ export declare function normalizeRules(rules: unknown): SessionPermissionRules;
188
+ /**
189
+ * THE ONE WRITE JUDGE every store applies before it stamps a row (both bundled stores, and a durable backend at the same point):
190
+ * the rules to stamp, or a {@link SessionPolicyError}, in this order — `conflict` (a stale `expectedRev` against `prior`'s rev),
191
+ * `invalid_shape` ({@link normalizeRules}), `inert_entry` (an entry the reader reads as nothing; operator writes included — a
192
+ * rule that names nothing guards nothing whoever writes it), then, for a non-operator write over an existing row,
193
+ * `loosen_forbidden` ({@link loosenReasons}) — or `corrupt` when that prior cannot be read as rules, which the comparison never
194
+ * reads as fewer. `mountedToolNames` = the store's declaration ({@link SessionPolicyStoreOptions.mountedToolNames}).
195
+ */
196
+ export declare function judgeSessionRulesWrite(prior: StoredSessionRules | null, rules: unknown, opts: PutRulesOptions | undefined, mountedToolNames?: ReadonlySet<string>): SessionPermissionRules;
197
+ /**
198
+ * Pure tighten-only diff: the reasons `next` LOOSENS `prior` (empty ⇒ `next` is a pure tightening or no-op), over the EFFECTIVE
199
+ * entries of each rule bucket — the list as the run-time layer enforces it ({@link effectiveEntries}). An entry that reads as
200
+ * nothing is in neither side's effect, so removing one relaxes nothing and adding one widens nothing (the way a session's owner
201
+ * takes out an entry that names nothing); an allowlist a `*` voids admits nothing, so dropping the `*` widens. toolDeny: removing an effective entry loosens. toolAllow: removing the list
202
+ * (→ everything allowed) or adding an effective entry outside the prior set loosens; prior-undefined → any defined next only
203
+ * narrows. allowDirs: removing confinement, or adding a dir not within some prior dir, loosens (coarse raw containment).
204
+ * Entries compare as written. Both sides are read through {@link readSessionRulesShape} first: a side it cannot read (a key that
205
+ * is no bucket — a retired one included) throws `invalid_shape` rather than compare without it. `mountedToolNames` = the store's
206
+ * declaration ({@link SessionPolicyStoreOptions.mountedToolNames}; empty when it has none) — the write judge's too.
188
207
  */
189
- export declare function loosenReasons(prior: SessionPermissionRules, next: SessionPermissionRules, mountedToolNames?: ReadonlySet<string>): string[];
208
+ export declare function loosenReasons(priorValue: SessionPermissionRules, nextValue: SessionPermissionRules, mountedToolNames?: ReadonlySet<string>): string[];
190
209
  /**
191
210
  * Does row `a` belong to the SAME lineage as row `b` — the one question every reader asks before it compares
192
211
  * two rows' `rev`, asked in ONE place so a `gen` that is ABSENT cannot mean three different things at three
@@ -205,8 +224,6 @@ export declare function continuesLineage(a: {
205
224
  }, b: {
206
225
  gen?: number;
207
226
  }): boolean;
208
- /** strip any caller-passed `rev` from a rules object so the store is the sole authority on it. */
209
- export declare function stripRev(rules: SessionPermissionRules): SessionPermissionRules;
210
227
  /**
211
228
  * In-process default {@link SessionPolicyStore} (single-replica). A deployment that needs cross-replica
212
229
  * durability supplies its own backend with the SAME CAS-rev + tighten-only semantics. Single-threaded JS makes
@@ -1,7 +1,12 @@
1
1
  import { posix } from "node:path";
2
2
  import { isWinFormPath } from "../tools/fs/safety.js";
3
3
  import { NAMESPACED_NAME_SHAPES, carriesNamespaceSeparator } from "./protocol-table.js";
4
- import { judgeRuleToolName } from "./tool-name-aliases.js";
4
+ import { compileAllowList, inertEntryPhrase, readRuleEntry, renderListEntry } from "./allow-entry.js";
5
+ import { valueTypeLabel } from "./tool-name-list.js";
6
+ const SESSION_RULE_BUCKETS = { toolAllow: "allow", toolDeny: "deny", allowDirs: "dirs" };
7
+ const BUCKETS = Object.keys(SESSION_RULE_BUCKETS);
8
+ const ENTRY_FIELDS = BUCKETS.filter((f) => SESSION_RULE_BUCKETS[f] !== "dirs");
9
+ const STORE_STAMPS = new Set(["rev", "gen"]);
5
10
  export class SessionPolicyError extends Error {
6
11
  code;
7
12
  detail;
@@ -12,8 +17,6 @@ export class SessionPolicyError extends Error {
12
17
  this.name = "SessionPolicyError";
13
18
  }
14
19
  }
15
- const HOLDS_TOOL_NAMES = { toolAllow: true, toolDeny: true, allowDirs: false, commandAllow: false, commandDeny: false };
16
- const TOOL_NAME_FIELDS = Object.keys(HOLDS_TOOL_NAMES).filter((f) => HOLDS_TOOL_NAMES[f]);
17
20
  const NO_DECLARED_TOOLS = new Set();
18
21
  export function readMountedToolNames(value) {
19
22
  if (value === undefined)
@@ -33,35 +36,6 @@ export function readMountedToolNames(value) {
33
36
  }
34
37
  return names;
35
38
  }
36
- export function assertSessionRuleNamesCurrent(rules, mountedToolNames = NO_DECLARED_TOOLS) {
37
- for (const field of TOOL_NAME_FIELDS) {
38
- for (const name of rules[field] ?? []) {
39
- const verdict = judgeRuleToolName(name, mountedToolNames);
40
- if (verdict.kind === "current")
41
- continue;
42
- const quoted = JSON.stringify(name);
43
- const successor = verdict.kind === "unprefixed_namespace" ? undefined : verdict.currentName;
44
- const guidance = verdict.kind === "unprefixed_namespace" ? undefined : verdict.guidance;
45
- let message;
46
- switch (verdict.kind) {
47
- case "retired":
48
- message = `session rules ${field} names ${quoted}, a retired tool name (${verdict.guidance}) — with it in place no run of this session could start, so nothing was written.${successor !== undefined ? ` Use the current name ${JSON.stringify(successor)}.` : ""} If this deployment mounts its own tool under ${quoted}, declare it in the store's mountedToolNames.`;
49
- break;
50
- case "unprefixed_namespace":
51
- message = `session rules ${field} names ${quoted}, which carries the "__" namespace separator but no protocol prefix — no tool is named that way (protocol tools are named ${NAMESPACED_NAME_SHAPES}), and with it in place no run of this session could start, so nothing was written.`;
52
- break;
53
- case "unmintable_segment":
54
- message = `session rules ${field} names ${quoted}, which can match no tool (${verdict.guidance}) — with it in place no run of this session could start, so nothing was written.${successor !== undefined ? ` Write ${JSON.stringify(successor)}.` : ""}`;
55
- break;
56
- default: {
57
- const exhaustive = verdict;
58
- return exhaustive;
59
- }
60
- }
61
- throw new SessionPolicyError("legacy_tool_name", message, { field, name, verdict: verdict.kind, ...(guidance !== undefined ? { guidance } : {}), ...(successor !== undefined ? { currentName: successor } : {}) });
62
- }
63
- }
64
- }
65
39
  function lexWithin(root, p) {
66
40
  const fold = (x) => {
67
41
  let n = isWinFormPath(x) ? x.replace(/\\/g, "/") : x;
@@ -75,61 +49,109 @@ function lexWithin(root, p) {
75
49
  return true;
76
50
  return np === nr || np.startsWith(nr + "/");
77
51
  }
78
- function ruleList(field, value) {
79
- if (value === undefined)
80
- return undefined;
81
- const refuse = (got) => {
82
- throw new TypeError(`session rules ${field} must be an array of strings, got ${got} — a rule list is written as a list of names (["rm"], not "rm")`);
83
- };
84
- if (Array.isArray(value) && Array.from(value).every((entry) => typeof entry === "string")) {
85
- if (value.some((entry) => entry.trim() === ""))
86
- refuse("an array holding an empty or blank entry");
87
- return value;
52
+ export function readSessionRulesShape(value) {
53
+ const fault = (field, got, sentence) => ({ fault: { field, got }, sentence });
54
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
55
+ const got = Array.isArray(value) ? "an array" : valueTypeLabel(value);
56
+ return fault("", got, `session rules must be an object of rule buckets (${BUCKETS.join(", ")}), got ${got}`);
88
57
  }
89
- return refuse(Array.isArray(value) ? "an array holding an entry that is not a string" : value === null ? "null" : `a ${typeof value}`);
58
+ const rules = {};
59
+ for (const [key, v] of Object.entries(value)) {
60
+ if (STORE_STAMPS.has(key) || v === undefined)
61
+ continue;
62
+ if (!Object.hasOwn(SESSION_RULE_BUCKETS, key)) {
63
+ return fault(key, "not a rule bucket", `session rules hold ${JSON.stringify(key)}, which is not a rule bucket — the buckets are ${BUCKETS.join(", ")}, and a shell command rule is a toolDeny / toolAllow entry written Bash(<command>) (Bash(rm:*) refuses every run of rm)`);
64
+ }
65
+ const field = key;
66
+ const listed = Array.isArray(v) ? Array.from(v) : undefined;
67
+ const got = listed === undefined
68
+ ? valueTypeLabel(v)
69
+ : !listed.every((e) => typeof e === "string")
70
+ ? "an array holding an entry that is not a string"
71
+ : SESSION_RULE_BUCKETS[field] === "dirs" && listed.some((e) => e.trim() === "")
72
+ ? "an array holding an empty or blank entry"
73
+ : undefined;
74
+ if (got !== undefined)
75
+ return fault(field, got, `session rules ${field} must be an array of ${SESSION_RULE_BUCKETS[field] === "dirs" ? "directory paths" : "rule entries"}, got ${got} — a bucket is written as a list (["Bash(rm:*)"], not "Bash(rm:*)")`);
76
+ rules[field] = listed;
77
+ }
78
+ return { rules };
90
79
  }
91
80
  export function normalizeRules(rules) {
81
+ const read = readSessionRulesShape(rules);
82
+ if ("fault" in read)
83
+ throw new SessionPolicyError("invalid_shape", read.sentence, read.fault);
92
84
  const out = {};
93
- const toolAllow = ruleList("toolAllow", rules.toolAllow);
94
- const toolDeny = ruleList("toolDeny", rules.toolDeny);
95
- const commandAllow = ruleList("commandAllow", rules.commandAllow);
96
- const commandDeny = ruleList("commandDeny", rules.commandDeny);
97
- const allowDirs = ruleList("allowDirs", rules.allowDirs);
98
- if (toolAllow !== undefined)
99
- out.toolAllow = [...toolAllow];
100
- if (toolDeny !== undefined)
101
- out.toolDeny = [...toolDeny];
102
- if (commandAllow !== undefined)
103
- out.commandAllow = [...commandAllow];
104
- if (commandDeny !== undefined)
105
- out.commandDeny = [...commandDeny];
106
- if (allowDirs !== undefined)
107
- out.allowDirs = allowDirs.map((d) => posix.normalize(d).replace(/\/+$/, "") || "/");
85
+ for (const field of BUCKETS) {
86
+ const list = read.rules[field];
87
+ if (list === undefined)
88
+ continue;
89
+ out[field] = SESSION_RULE_BUCKETS[field] === "dirs" ? list.map((d) => posix.normalize(d).replace(/\/+$/, "") || "/") : [...list];
90
+ }
108
91
  return out;
109
92
  }
110
- export function loosenReasons(prior, next, mountedToolNames = NO_DECLARED_TOOLS) {
111
- const reasons = [];
112
- for (const field of ["toolDeny", "commandDeny"]) {
113
- const n = new Set(next[field] ?? []);
114
- for (const x of prior[field] ?? []) {
115
- if (n.has(x) || (field === "toolDeny" && judgeRuleToolName(x, mountedToolNames).kind !== "current"))
93
+ function effectiveEntries(field, list, mountedToolNames) {
94
+ return compileAllowList(list ?? [], SESSION_RULE_BUCKETS[field], mountedToolNames).rows.map((r) => r.entry);
95
+ }
96
+ function assertEntriesRead(rules, mountedToolNames) {
97
+ for (const field of ENTRY_FIELDS) {
98
+ for (const entry of rules[field] ?? []) {
99
+ const read = readRuleEntry(entry, SESSION_RULE_BUCKETS[field], mountedToolNames);
100
+ if (read.kind === "row")
116
101
  continue;
117
- reasons.push(`removed ${field} "${x}"`);
102
+ const { kind: _kind, ...why } = read;
103
+ void _kind;
104
+ const way = why.verdict === "retired"
105
+ ? ` If this deployment mounts its own tool under ${renderListEntry(entry)}, declare it in the store's mountedToolNames.`
106
+ : why.currentName === undefined
107
+ ? ` An entry is a tool name (Read; a protocol tool is named ${NAMESPACED_NAME_SHAPES}), a protocol namespace (mcp__<server>), or a rule in its tool's own grammar (Bash(<command>), Edit(<path>), Skill(<name>), WebFetch(domain:<host>)).`
108
+ : "";
109
+ const sentence = why.guidance !== undefined ? ` — ${why.guidance}` : "";
110
+ throw new SessionPolicyError("inert_entry", `session rules ${field} holds ${inertEntryPhrase({ entry, ...why })}, which ${SESSION_RULE_BUCKETS[field] === "deny" ? "refuses" : "admits"} nothing as written${sentence}; nothing was written.${way}`, { field, entry, ...why });
118
111
  }
119
112
  }
120
- for (const field of ["toolAllow", "commandAllow"]) {
121
- const p = prior[field];
122
- if (p === undefined)
123
- continue;
124
- const n = next[field];
125
- if (n === undefined) {
126
- reasons.push(`removed ${field} allowlist (all allowed)`);
127
- continue;
113
+ }
114
+ export function judgeSessionRulesWrite(prior, rules, opts, mountedToolNames = NO_DECLARED_TOOLS) {
115
+ if (opts?.expectedRev !== undefined && (prior?.rev ?? 0) !== opts.expectedRev) {
116
+ throw new SessionPolicyError("conflict", `session rules rev mismatch (expected ${opts.expectedRev}, have ${prior?.rev ?? 0})`);
117
+ }
118
+ const clean = normalizeRules(rules);
119
+ assertEntriesRead(clean, mountedToolNames);
120
+ if (prior !== null && opts?.operator !== true) {
121
+ const stored = readSessionRulesShape(prior);
122
+ if ("fault" in stored) {
123
+ throw new SessionPolicyError("corrupt", `the stored session rules cannot be read as rules (${stored.sentence}) — refusing to judge a non-operator write against them as fewer rules; an operator write replaces them`);
124
+ }
125
+ const loosens = loosenReasons(stored.rules, clean, mountedToolNames);
126
+ if (loosens.length > 0) {
127
+ throw new SessionPolicyError("loosen_forbidden", `a non-operator session-rule write may only tighten; refused: ${loosens.join("; ")}`);
128
+ }
129
+ }
130
+ return clean;
131
+ }
132
+ export function loosenReasons(priorValue, nextValue, mountedToolNames = NO_DECLARED_TOOLS) {
133
+ const shapeOf = (side, value) => {
134
+ const read = readSessionRulesShape(value);
135
+ if ("fault" in read)
136
+ throw new SessionPolicyError("invalid_shape", `the ${side} rules: ${read.sentence}`, read.fault);
137
+ return read.rules;
138
+ };
139
+ const prior = shapeOf("prior", priorValue);
140
+ const next = shapeOf("next", nextValue);
141
+ const reasons = [];
142
+ const keptDeny = new Set(effectiveEntries("toolDeny", next.toolDeny, mountedToolNames));
143
+ for (const x of effectiveEntries("toolDeny", prior.toolDeny, mountedToolNames))
144
+ if (!keptDeny.has(x))
145
+ reasons.push(`removed toolDeny ${renderListEntry(x)}`);
146
+ if (prior.toolAllow !== undefined) {
147
+ if (next.toolAllow === undefined)
148
+ reasons.push("removed toolAllow allowlist (all allowed)");
149
+ else {
150
+ const had = new Set(effectiveEntries("toolAllow", prior.toolAllow, mountedToolNames));
151
+ for (const x of effectiveEntries("toolAllow", next.toolAllow, mountedToolNames))
152
+ if (!had.has(x))
153
+ reasons.push(`widened toolAllow with ${renderListEntry(x)}`);
128
154
  }
129
- const ps = new Set(p);
130
- for (const x of n)
131
- if (!ps.has(x))
132
- reasons.push(`widened ${field} with "${x}"`);
133
155
  }
134
156
  const pd = prior.allowDirs;
135
157
  if (pd !== undefined && pd.length > 0) {
@@ -146,11 +168,6 @@ export function loosenReasons(prior, next, mountedToolNames = NO_DECLARED_TOOLS)
146
168
  export function continuesLineage(a, b) {
147
169
  return (a.gen ?? 0) === (b.gen ?? 0);
148
170
  }
149
- export function stripRev(rules) {
150
- const { ...rest } = rules;
151
- delete rest.rev;
152
- return rest;
153
- }
154
171
  export class InMemorySessionPolicyStore {
155
172
  map = new Map();
156
173
  mountedToolNames;
@@ -168,17 +185,7 @@ export class InMemorySessionPolicyStore {
168
185
  async putRules(sessionId, principal, rules, opts) {
169
186
  const key = this.key(sessionId, principal);
170
187
  const prior = this.map.get(key);
171
- if (opts?.expectedRev !== undefined && (prior?.rev ?? 0) !== opts.expectedRev) {
172
- throw new SessionPolicyError("conflict", `session rules rev mismatch (expected ${opts.expectedRev}, have ${prior?.rev ?? 0})`);
173
- }
174
- const clean = normalizeRules(stripRev(rules));
175
- assertSessionRuleNamesCurrent(clean, this.mountedToolNames);
176
- if (prior && opts?.operator !== true) {
177
- const loosens = loosenReasons(prior, clean, this.mountedToolNames);
178
- if (loosens.length > 0) {
179
- throw new SessionPolicyError("loosen_forbidden", `a non-operator session-rule write may only tighten; refused: ${loosens.join("; ")}`);
180
- }
181
- }
188
+ const clean = judgeSessionRulesWrite(prior ?? null, rules, opts, this.mountedToolNames);
182
189
  const mark = this.highWater.get(key) ?? { rev: 0, gen: 0 };
183
190
  const floor = Math.max(prior?.rev ?? 0, mark.rev);
184
191
  const gen = prior?.gen ?? mark.gen;
@@ -1,6 +1,40 @@
1
1
  import type { ShellReadVerdict } from "../tools/fs/shell-read-check.js";
2
+ import { type ShellReadDisposition } from "../tools/fs/shell-read-judge.js";
2
3
  import type { GatePass } from "./runner/gate-exit.js";
3
4
  import type { AskGuard } from "./tool-policy.js";
5
+ /** One station answer, received and read: what a reader acts on (`kind`) and the answer it acts on (`verdict` — the engine's own
6
+ * copy, received once). `unbelieved`: the station's answer was not received or contradicted its own finding, and `verdict` is the
7
+ * engine's refusal of it (its sentence names the station's words only where they are words of the closed sets — nothing a station
8
+ * spelled reaches the model), reported once on the call's lane ({@link StationAnswerCall.report}). */
9
+ export interface StationAnswerReading {
10
+ readonly kind: ShellReadDisposition | "silence";
11
+ readonly verdict: ShellReadVerdict;
12
+ readonly unbelieved?: true;
13
+ }
14
+ /** The call a reader reads an answer for: its name (the refusal names it), its id, and the lane a station's failure is reported on
15
+ * (absent ⇒ unreported — the resumed leg reads the engine's own stations). */
16
+ export interface StationAnswerCall {
17
+ readonly toolName: string;
18
+ readonly toolCallId?: string;
19
+ readonly report?: (err: Error) => void;
20
+ }
21
+ /**
22
+ * Receive and read a station's answer — the ONE reading for every reader. The answer is received whole ({@link receivedAnswer}:
23
+ * the engine's one receipt for a caller's object, then each member by its kind — none of the caller's code runs) into the engine's
24
+ * copy every later read is of; an answer that is not received (an accessor, a member its prototype or a non-enumerable property
25
+ * supplies, a Proxy anywhere in it, a word that is no string, a mark that is not `true`, not an object) is not believed. Then, in order:
26
+ * · `refuse` stands, whatever beside it: no reading is stricter;
27
+ * · the station's SILENCE — the finding `unanswered`, or nothing found (`none`) beside a silence mark (`environmentSilent`,
28
+ * `rowsUnread`): the station made no finding, whatever disposition it spelled beside it (the engine's own station spells a
29
+ * vouch beside unread rows as `unanswered`);
30
+ * · a disposition the decision table does not give the finding on any seat, under any form of the question
31
+ * ({@link dispositionsOfRow}) — a `vouch` beside a deny hit, an ask beside one, a word outside the closed sets — is NOT believed:
32
+ * the station's answer contradicts its own finding, and the reader refuses the call;
33
+ * · otherwise the disposition, as the table gives it.
34
+ * A silence mark beside a finding the station DID make is not the silence: it classes that finding's question
35
+ * ({@link silencedBy}).
36
+ */
37
+ export declare function readStationAnswer(answer: ShellReadVerdict, call: StationAnswerCall): StationAnswerReading;
4
38
  /** 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
39
  * not be read (the rule lane's class); the finding `unanswered` is, unless rows are unread, the environment's silence — shell text the
6
40
  * engine cannot read (a guard) — and the marks only pick whose. Either is a question only a person answers; `probe` follows the finding. */
@@ -12,10 +46,10 @@ export declare function silencedBy(verdict: ShellReadVerdict, d: {
12
46
  guards?: readonly AskGuard[];
13
47
  requiresRealApproval?: true;
14
48
  };
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. */
49
+ /** The station's SILENCE ({@link readStationAnswer}), applied once, before any disposition arm: a station — a host-mounted one
50
+ * included — may spell `vouch`, `mandated` or `ordinary` beside it, and no word vouches for what nobody judged. A deny stands; an
51
+ * ask keeps its words and gains the silence's class; an allow becomes the silence's question (`ask.message`), one only a person
52
+ * answers. Nothing is admitted, no `probe` is stamped and no grant is offered — the station made no finding. */
19
53
  export declare function applyUnansweredSilence(pass: GatePass, verdict: ShellReadVerdict, ask: {
20
54
  readonly message: string;
21
55
  readonly account: string | undefined;