approval-md 0.2.0 → 0.3.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 (235) hide show
  1. package/README.md +63 -24
  2. package/SPEC.md +57 -11
  3. package/dist/src/channels/contract.d.ts +34 -1
  4. package/dist/src/channels/contract.js +200 -7
  5. package/dist/src/channels/contract.js.map +1 -1
  6. package/dist/src/channels/telegram.d.ts +123 -11
  7. package/dist/src/channels/telegram.js +218 -23
  8. package/dist/src/channels/telegram.js.map +1 -1
  9. package/dist/src/channels/web.d.ts +9 -0
  10. package/dist/src/channels/web.js +17 -0
  11. package/dist/src/channels/web.js.map +1 -1
  12. package/dist/src/cli/amend.js +214 -30
  13. package/dist/src/cli/amend.js.map +1 -1
  14. package/dist/src/cli/attest.d.ts +9 -0
  15. package/dist/src/cli/attest.js +134 -7
  16. package/dist/src/cli/attest.js.map +1 -1
  17. package/dist/src/cli/channel-telegram.d.ts +99 -26
  18. package/dist/src/cli/channel-telegram.js +311 -13
  19. package/dist/src/cli/channel-telegram.js.map +1 -1
  20. package/dist/src/cli/channel.d.ts +9 -0
  21. package/dist/src/cli/channel.js +9 -0
  22. package/dist/src/cli/channel.js.map +1 -1
  23. package/dist/src/cli/codex-bridge.d.ts +819 -0
  24. package/dist/src/cli/codex-bridge.js +1607 -0
  25. package/dist/src/cli/codex-bridge.js.map +1 -0
  26. package/dist/src/cli/codex.d.ts +1 -1
  27. package/dist/src/cli/codex.js +304 -7
  28. package/dist/src/cli/codex.js.map +1 -1
  29. package/dist/src/cli/daemon.js +4 -1
  30. package/dist/src/cli/daemon.js.map +1 -1
  31. package/dist/src/cli/doctor.js +467 -12
  32. package/dist/src/cli/doctor.js.map +1 -1
  33. package/dist/src/cli/execute.js +25 -2
  34. package/dist/src/cli/execute.js.map +1 -1
  35. package/dist/src/cli/help.d.ts +6 -2
  36. package/dist/src/cli/help.js +165 -60
  37. package/dist/src/cli/help.js.map +1 -1
  38. package/dist/src/cli/hook-codex.d.ts +49 -1
  39. package/dist/src/cli/hook-codex.js +60 -1
  40. package/dist/src/cli/hook-codex.js.map +1 -1
  41. package/dist/src/cli/hook.d.ts +459 -3
  42. package/dist/src/cli/hook.js +1062 -114
  43. package/dist/src/cli/hook.js.map +1 -1
  44. package/dist/src/cli/import.js +1 -1
  45. package/dist/src/cli/import.js.map +1 -1
  46. package/dist/src/cli/main.js +5 -3
  47. package/dist/src/cli/main.js.map +1 -1
  48. package/dist/src/cli/policy-apply.d.ts +195 -0
  49. package/dist/src/cli/policy-apply.js +573 -0
  50. package/dist/src/cli/policy-apply.js.map +1 -0
  51. package/dist/src/cli/policy.js +14 -1
  52. package/dist/src/cli/policy.js.map +1 -1
  53. package/dist/src/cli/preflight.d.ts +151 -13
  54. package/dist/src/cli/preflight.js +398 -41
  55. package/dist/src/cli/preflight.js.map +1 -1
  56. package/dist/src/cli/sandbox.js +17 -1
  57. package/dist/src/cli/sandbox.js.map +1 -1
  58. package/dist/src/cli/scaffold.d.ts +1 -1
  59. package/dist/src/cli/scaffold.js +1 -1
  60. package/dist/src/cli/setup-channel.d.ts +9 -0
  61. package/dist/src/cli/setup-channel.js +28 -1
  62. package/dist/src/cli/setup-channel.js.map +1 -1
  63. package/dist/src/cli/setup-common.d.ts +3 -1
  64. package/dist/src/cli/setup-common.js +3 -2
  65. package/dist/src/cli/setup-common.js.map +1 -1
  66. package/dist/src/cli/setup.d.ts +2 -0
  67. package/dist/src/cli/setup.js +94 -2
  68. package/dist/src/cli/setup.js.map +1 -1
  69. package/dist/src/cli/up.js +115 -51
  70. package/dist/src/cli/up.js.map +1 -1
  71. package/dist/src/cli/values.js +3 -4
  72. package/dist/src/cli/values.js.map +1 -1
  73. package/dist/src/cli/verb-registry.js +174 -9
  74. package/dist/src/cli/verb-registry.js.map +1 -1
  75. package/dist/src/cli/wordmark.d.ts +2 -2
  76. package/dist/src/cli/wordmark.js +2 -2
  77. package/dist/src/codex/broker.d.ts +229 -0
  78. package/dist/src/codex/broker.js +548 -0
  79. package/dist/src/codex/broker.js.map +1 -0
  80. package/dist/src/codex/runner.d.ts +178 -0
  81. package/dist/src/codex/runner.js +231 -0
  82. package/dist/src/codex/runner.js.map +1 -0
  83. package/dist/src/codex/serve.d.ts +56 -0
  84. package/dist/src/codex/serve.js +98 -0
  85. package/dist/src/codex/serve.js.map +1 -0
  86. package/dist/src/codex/workspace-commit.d.ts +219 -0
  87. package/dist/src/codex/workspace-commit.js +549 -0
  88. package/dist/src/codex/workspace-commit.js.map +1 -0
  89. package/dist/src/core/advance-cycle.d.ts +51 -0
  90. package/dist/src/core/advance-cycle.js +66 -2
  91. package/dist/src/core/advance-cycle.js.map +1 -1
  92. package/dist/src/core/agents-md.d.ts +20 -18
  93. package/dist/src/core/agents-md.js +33 -31
  94. package/dist/src/core/agents-md.js.map +1 -1
  95. package/dist/src/core/attest.d.ts +215 -0
  96. package/dist/src/core/attest.js +317 -7
  97. package/dist/src/core/attest.js.map +1 -1
  98. package/dist/src/core/audit.d.ts +18 -0
  99. package/dist/src/core/audit.js +13 -0
  100. package/dist/src/core/audit.js.map +1 -1
  101. package/dist/src/core/channel-owner.d.ts +213 -0
  102. package/dist/src/core/channel-owner.js +358 -0
  103. package/dist/src/core/channel-owner.js.map +1 -0
  104. package/dist/src/core/command-class.d.ts +154 -0
  105. package/dist/src/core/command-class.js +673 -20
  106. package/dist/src/core/command-class.js.map +1 -1
  107. package/dist/src/core/commit-guard.d.ts +272 -0
  108. package/dist/src/core/commit-guard.js +424 -0
  109. package/dist/src/core/commit-guard.js.map +1 -0
  110. package/dist/src/core/daemon-actor.d.ts +45 -0
  111. package/dist/src/core/daemon-actor.js +54 -0
  112. package/dist/src/core/daemon-actor.js.map +1 -0
  113. package/dist/src/core/dark-session.d.ts +109 -8
  114. package/dist/src/core/dark-session.js +266 -82
  115. package/dist/src/core/dark-session.js.map +1 -1
  116. package/dist/src/core/decision-refusal.d.ts +23 -2
  117. package/dist/src/core/decision-refusal.js +24 -2
  118. package/dist/src/core/decision-refusal.js.map +1 -1
  119. package/dist/src/core/env-file.d.ts +5 -0
  120. package/dist/src/core/env-file.js +60 -1
  121. package/dist/src/core/env-file.js.map +1 -1
  122. package/dist/src/core/execute.d.ts +15 -2
  123. package/dist/src/core/execute.js +15 -2
  124. package/dist/src/core/execute.js.map +1 -1
  125. package/dist/src/core/gate.d.ts +86 -1
  126. package/dist/src/core/gate.js +81 -1
  127. package/dist/src/core/gate.js.map +1 -1
  128. package/dist/src/core/gesture-refusal.d.ts +166 -0
  129. package/dist/src/core/gesture-refusal.js +188 -0
  130. package/dist/src/core/gesture-refusal.js.map +1 -0
  131. package/dist/src/core/harness-version.d.ts +1 -1
  132. package/dist/src/core/harness-version.js +3 -1
  133. package/dist/src/core/harness-version.js.map +1 -1
  134. package/dist/src/core/instance.d.ts +59 -2
  135. package/dist/src/core/instance.js +113 -0
  136. package/dist/src/core/instance.js.map +1 -1
  137. package/dist/src/core/log.d.ts +39 -1
  138. package/dist/src/core/log.js.map +1 -1
  139. package/dist/src/core/policy-explain.d.ts +10 -0
  140. package/dist/src/core/policy-explain.js +32 -0
  141. package/dist/src/core/policy-explain.js.map +1 -1
  142. package/dist/src/core/policy-load.d.ts +41 -1
  143. package/dist/src/core/policy-load.js +21 -3
  144. package/dist/src/core/policy-load.js.map +1 -1
  145. package/dist/src/core/policy-match.d.ts +43 -0
  146. package/dist/src/core/policy-match.js +52 -0
  147. package/dist/src/core/policy-match.js.map +1 -1
  148. package/dist/src/core/policy-proposal.d.ts +52 -0
  149. package/dist/src/core/policy-proposal.js +102 -2
  150. package/dist/src/core/policy-proposal.js.map +1 -1
  151. package/dist/src/core/protected-path-guard.d.ts +117 -4
  152. package/dist/src/core/protected-path-guard.js +362 -48
  153. package/dist/src/core/protected-path-guard.js.map +1 -1
  154. package/dist/src/core/question-preempted.d.ts +141 -0
  155. package/dist/src/core/question-preempted.js +152 -0
  156. package/dist/src/core/question-preempted.js.map +1 -0
  157. package/dist/src/core/read-scope.d.ts +172 -0
  158. package/dist/src/core/read-scope.js +252 -0
  159. package/dist/src/core/read-scope.js.map +1 -0
  160. package/dist/src/core/sandbox.d.ts +81 -0
  161. package/dist/src/core/sandbox.js +190 -1
  162. package/dist/src/core/sandbox.js.map +1 -1
  163. package/dist/src/core/sender-identity.d.ts +476 -0
  164. package/dist/src/core/sender-identity.js +572 -0
  165. package/dist/src/core/sender-identity.js.map +1 -0
  166. package/dist/src/core/shlex.d.ts +102 -0
  167. package/dist/src/core/shlex.js +159 -0
  168. package/dist/src/core/shlex.js.map +1 -0
  169. package/dist/src/core/values.d.ts +18 -8
  170. package/dist/src/core/values.js +36 -1
  171. package/dist/src/core/values.js.map +1 -1
  172. package/dist/src/daemon/advance.d.ts +10 -0
  173. package/dist/src/daemon/advance.js +25 -4
  174. package/dist/src/daemon/advance.js.map +1 -1
  175. package/dist/src/daemon/daemon.js +9 -0
  176. package/dist/src/daemon/daemon.js.map +1 -1
  177. package/dist/src/daemon/git-evidence.d.ts +2 -2
  178. package/dist/src/daemon/git-evidence.js +1 -1
  179. package/dist/src/mcp/server.js +8 -0
  180. package/dist/src/mcp/server.js.map +1 -1
  181. package/docs/cli-reference.md +932 -32
  182. package/docs/codex-enforced-session.md +75 -2
  183. package/docs/codex-workspace-broker.md +118 -0
  184. package/package.json +3 -1
  185. package/schema/event.schema.json +538 -9
  186. package/schema/fixtures/event/invalid/approval-granted-sender-hashed-false.json +20 -0
  187. package/schema/fixtures/event/invalid/approval-granted-sender-hashed-raw-id.json +20 -0
  188. package/schema/fixtures/event/invalid/audit-gesture-refused-human-actor.json +16 -0
  189. package/schema/fixtures/event/invalid/audit-gesture-refused-no-actor-no-sender.json +15 -0
  190. package/schema/fixtures/event/invalid/audit-gesture-refused-unknown-gesture.json +16 -0
  191. package/schema/fixtures/event/invalid/audit-question-preempted-agent-actor.json +16 -0
  192. package/schema/fixtures/event/invalid/audit-question-preempted-no-question-id.json +16 -0
  193. package/schema/fixtures/event/invalid/audit-question-preempted-unknown-source.json +15 -0
  194. package/schema/fixtures/event/invalid/gate-path-signed-off-absolute-path.json +14 -0
  195. package/schema/fixtures/event/invalid/gate-path-signed-off-agent-actor.json +14 -0
  196. package/schema/fixtures/event/invalid/gate-path-signed-off-missing-path.json +13 -0
  197. package/schema/fixtures/event/valid/approval-granted-sender-hashed.json +20 -0
  198. package/schema/fixtures/event/valid/audit-gesture-refused-review-note.json +21 -0
  199. package/schema/fixtures/event/valid/audit-gesture-refused-sender-key-unavailable.json +19 -0
  200. package/schema/fixtures/event/valid/audit-gesture-refused.json +19 -0
  201. package/schema/fixtures/event/valid/audit-question-preempted-no-verdict.json +16 -0
  202. package/schema/fixtures/event/valid/audit-question-preempted.json +20 -0
  203. package/schema/fixtures/event/valid/gate-path-signed-off.json +14 -0
  204. package/schema/fixtures/event/valid/harness-kind-claude-code.json +23 -0
  205. package/schema/fixtures/event/valid/harness-kind-codex.json +23 -0
  206. package/schema/fixtures/event/valid/harness-kind-cursor.json +23 -0
  207. package/schema/fixtures/event/valid/harness-kind-grok.json +23 -0
  208. package/schema/fixtures/event/valid/harness-kind-muse.json +23 -0
  209. package/schema/fixtures/policy/invalid/senders-half-keyed.json +20 -0
  210. package/schema/fixtures/policy/valid/canonical.json +1 -1
  211. package/schema/fixtures/policy/valid/senders-keyed.json +24 -0
  212. package/schema/fixtures/policy-md/valid/canonical.md +1 -1
  213. package/schema/fixtures/policy-md/valid/with-values.md +5 -7
  214. package/schema/fixtures/values/invalid/class-shaped.json +1 -1
  215. package/schema/fixtures/values/invalid/duplicate-entry.json +1 -1
  216. package/schema/fixtures/values/invalid/non-string-item.json +1 -1
  217. package/schema/fixtures/values/invalid/over-cap.json +1 -1
  218. package/schema/fixtures/values/invalid/unknown-key.json +1 -1
  219. package/schema/fixtures/values/invalid/version-float.json +1 -0
  220. package/schema/fixtures/values/invalid/version-integer.json +1 -0
  221. package/schema/fixtures/values/invalid/version-wrong-string.json +1 -0
  222. package/schema/fixtures/values/valid/empty-lists.json +2 -3
  223. package/schema/fixtures/values/valid/full.json +5 -7
  224. package/schema/fixtures/values/valid/minimal.json +1 -1
  225. package/schema/fixtures/values-md/invalid/schema-invalid.md +5 -3
  226. package/schema/fixtures/values-md/invalid/two-blocks.md +3 -3
  227. package/schema/fixtures/values-md/invalid/unterminated.md +2 -2
  228. package/schema/fixtures/values-md/invalid/version-1.md +69 -0
  229. package/schema/fixtures/values-md/invalid/version-unquoted.md +64 -0
  230. package/schema/fixtures/values-md/invalid/yaml-error.md +2 -2
  231. package/schema/fixtures/values-md/valid/absent.md +1 -1
  232. package/schema/fixtures/values-md/valid/with-values.md +5 -7
  233. package/schema/policy.schema.json +54 -2
  234. package/schema/values.schema.json +7 -11
  235. package/schema/fixtures/values/invalid/version-string.json +0 -1
@@ -0,0 +1,476 @@
1
+ /**
2
+ * From an authenticated channel sender to an attested human identity
3
+ * (APRV-324, `design/channel-sender-identity.md`, amended SPEC.md §5.2/§10.3).
4
+ *
5
+ * ## The fact this module is about
6
+ *
7
+ * Before it, every Telegram tap was recorded against the actor the listener
8
+ * process was launched with. `routeCallback` checked `message.chat.id` against
9
+ * the configured chat and never read the update's `from` object at all, so two
10
+ * people in one chat both approved as one name and the log could not tell them
11
+ * apart. GitHub issue #137 asked for the person who tapped; this is the
12
+ * mapping that produces one.
13
+ *
14
+ * ## What is evidence here, and what is not
15
+ *
16
+ * Exactly one field in the whole system is a sender fact worth mapping:
17
+ * Telegram's `callback_query.from.id`, the stable numeric account id the Bot
18
+ * API attributes the tap to. {@link SENDER_CHANNELS} is closed to that one
19
+ * channel for that reason. A web form post authenticates nobody and a CLI
20
+ * process authenticates local machine control, so neither may be given an
21
+ * identity key it cannot support: a `senders` entry for either is a schema
22
+ * violation rather than a mapping nothing backs.
23
+ *
24
+ * Three things are deliberately NOT inputs:
25
+ *
26
+ * - **`from.username`.** Mutable and reusable, so a mapping keyed on it would
27
+ * transfer an identity with a handle. It is never a key and never recorded.
28
+ * - **Anything the message body says.** A callback whose payload or text names
29
+ * a user id is naming it about itself; SPEC.md §11.1 invariant 4 is the rule,
30
+ * and the whole difference between this design and a spoofable one is that
31
+ * the key is the transport's own attribution rather than a claim inside it.
32
+ * - **A sender on a channel with no transport authentication.** See above.
33
+ *
34
+ * And the mapping itself is evidence about an ACCOUNT, not about a person. What
35
+ * binds the account to a human is the operator's assertion, written in
36
+ * `APPROVAL.md`, which is `policy.core` and human-only and inoperative until a
37
+ * human re-attests it. That is why the mapping lives in the policy: it inherits
38
+ * the ceremony that already protects the approver roster it sits inside, and an
39
+ * agent can no more add itself as an approver's sender than as an approver.
40
+ *
41
+ * ## The four modes (design §3.2)
42
+ *
43
+ * {@link resolveSender} is total and pure, and returns one of:
44
+ *
45
+ * 1. `configured` — nothing to resolve against, so the surface keeps the actor
46
+ * it was launched with. This is today's behaviour, and it is what every
47
+ * deployment that never writes a `senders` key stays in forever.
48
+ * 2. `mapped` — the policy maps this sender to exactly one approver, and the
49
+ * decision is recorded as that person whatever the process was launched as.
50
+ * 3. `unmapped` — refuse. Never a fallback to the configured actor, which is
51
+ * today's behaviour dressed as a feature and would let a stranger in the
52
+ * chat approve as the operator.
53
+ * 4. `ambiguous` — two people claiming one account is an operator error, and
54
+ * the runtime does not resolve it by picking. `core/policy-load.ts` refuses
55
+ * such a policy at LOAD time, so this mode is the belt on that brace.
56
+ *
57
+ * ## Why mode 1 is keyed on the policy and not on the transport
58
+ *
59
+ * The channel supplies a sender for every tap once it can see one. If a
60
+ * supplied sender with nothing to map it against were a refusal, the first
61
+ * upgrade of the listener would lock every existing installation out of its own
62
+ * gate. So the question mode 1 asks is whether the POLICY configures a mapping
63
+ * for that channel at all: no approver declaring `senders.<channel>` means the
64
+ * operator has not adopted this, and the decision is attributed by
65
+ * configuration exactly as before. Writing the first mapping for a channel is
66
+ * what turns enforcement on for it, which is the migration `design §6`
67
+ * describes and `tests/sender-identity.test.ts` pins.
68
+ *
69
+ * ## A policy that does not load
70
+ *
71
+ * Fails closed: a supplied sender is refused `sender-unmapped`. A load failure
72
+ * is not "a policy with no mapping", it is a policy the runtime could not read,
73
+ * and inventing an identity from bytes it could not parse is the one thing this
74
+ * module exists to stop. Nothing is stranded by it — the CLI channel supplies
75
+ * no sender, so a terminal can still decide and still repair the file, which is
76
+ * where a `policy.core` edit has to happen anyway.
77
+ */
78
+ import type { PolicyLoadResult } from "./policy-load.js";
79
+ /**
80
+ * The prefix a KEYED sender mapping wears, in the policy and in the log
81
+ * (APRV-370).
82
+ *
83
+ * ## Why keyed, and not a plain digest
84
+ *
85
+ * The operator raised this on 2026-09-18 while applying APRV-324: a Telegram
86
+ * account id is a short decimal number, and this repository publishes both its
87
+ * policy and its log. Writing the raw id discloses the account once in
88
+ * `APPROVAL.md` and then on every phone decision. It is an identifier rather
89
+ * than a credential and the gate does not depend on its secrecy, so this is a
90
+ * disclosure question rather than a security hole, and the fix has to actually
91
+ * fix it: a plain `sha256:<hex>` of a ten-digit number is not a fix, because
92
+ * the whole space of ten-digit numbers is ten billion digests and a laptop
93
+ * enumerates it in minutes. The operator ruled on 2026-09-19 for the keyed
94
+ * form, which has no such space to enumerate without the key.
95
+ *
96
+ * ## What the key is, and what it is not
97
+ *
98
+ * An operator-held secret in the launch environment, beside the sampling
99
+ * secret, minted by `approval setup sender-key` and named by
100
+ * {@link SENDER_KEY_ENV}. It is NOT an authenticator: nothing about the gate's
101
+ * safety rests on it, and an attacker who learns it learns only which account
102
+ * ids the policy names, which is what the raw form told everybody anyway. It
103
+ * exists so that a published policy and a published log carry a value nobody
104
+ * can walk backwards.
105
+ */
106
+ export declare const SENDER_HASH_PREFIX = "hmac-sha256:";
107
+ /**
108
+ * The environment variable the sender key is read from.
109
+ *
110
+ * A CONVENTIONAL name rather than one the policy declares, which is the one
111
+ * place this diverges from the sampling secret's shape, and the reason is that
112
+ * the two questions differ. The policy names `audit.sampling_secret_env`
113
+ * because the POLICY decides whether sampling happens at all: a policy naming
114
+ * no variable turns the sampler off, and that is a deliberate control. Here the
115
+ * mapping's own form decides — a value wearing {@link SENDER_HASH_PREFIX} is
116
+ * keyed and a decimal one is not — so the policy already says everything it
117
+ * needs to, and a second declaration would be a second place for one fact to be
118
+ * wrong. Per-instance isolation comes from the env FILE beside the log, which
119
+ * is where the sampling secret gets it too.
120
+ */
121
+ export declare const SENDER_KEY_ENV = "APPROVAL_SENDER_KEY";
122
+ /** Is this mapping value (or recorded id) the keyed form? */
123
+ export declare function isHashedSenderId(value: string): boolean;
124
+ /**
125
+ * The keyed digest of one observed id: what a keyed policy carries and what a
126
+ * keyed record records.
127
+ *
128
+ * HMAC-SHA-256 under the operator's key, over the id as the transport reported
129
+ * it, hex, prefixed. Computed the way `core/sampler.ts` computes its selection
130
+ * value — `createHmac("sha256", key).update(value, "utf8")` — so this runtime
131
+ * has one keyed-digest idiom rather than two that could drift.
132
+ *
133
+ * The prefix is part of the value on purpose. It is what tells a reader of a
134
+ * policy or a log which form they are looking at without a second field to
135
+ * consult, and it is what makes a keyed entry and a raw entry unable to
136
+ * collide: a raw Telegram id is decimal digits and can never be this string.
137
+ */
138
+ export declare function hashedSenderId(key: string, id: string): string;
139
+ /** The sender key in this environment, or `null` when it is unset or empty. */
140
+ export declare function senderKeyFrom(env?: NodeJS.ProcessEnv): string | null;
141
+ /**
142
+ * The channels whose transport attributes a gesture to an account the operator
143
+ * can map (design §2).
144
+ *
145
+ * Closed, and short on purpose. `telegram` is here because the Bot API reports
146
+ * `callback_query.from.id`, a stable numeric account id the sender cannot
147
+ * choose. `web` and `cli` are absent because neither authenticates a person:
148
+ * the web page takes an unauthenticated form post and the CLI takes whoever
149
+ * controls the process. A policy that named either would be asserting a binding
150
+ * the runtime cannot check, so the schema refuses the key rather than carrying
151
+ * a mapping that reads like identity.
152
+ */
153
+ export declare const SENDER_CHANNELS: readonly ["telegram"];
154
+ export type SenderChannel = (typeof SENDER_CHANNELS)[number];
155
+ /** Is `name` a channel whose transport can attribute a gesture (§2)? */
156
+ export declare function isSenderChannel(name: string): name is SenderChannel;
157
+ /**
158
+ * One observed sender: the channel that saw it, and the id that channel's
159
+ * transport attributed the gesture to.
160
+ *
161
+ * `id` is always the transport's own attribution. For Telegram it is
162
+ * `String(callback_query.from.id)` and nothing else — not a username, not a
163
+ * chat id, and nothing read out of the message.
164
+ */
165
+ export interface ChannelSender {
166
+ /** The surface that observed it: `telegram`. */
167
+ channel: string;
168
+ /** The transport's stable account id, as a string. */
169
+ id: string;
170
+ }
171
+ /** Where a decision's actor came from, recorded on the decision (design §4). */
172
+ export declare const SENDER_SOURCES: readonly ["policy"];
173
+ export type SenderSource = (typeof SENDER_SOURCES)[number];
174
+ /**
175
+ * Refusals that belong to the decision SURFACE rather than to the gate
176
+ * (§11.1 invariant 6, amended SPEC.md §11.2).
177
+ *
178
+ * Its own union, deliberately. `GATE_REFUSAL_CODES` is documented as every way
179
+ * `approval register|request|decide|withdraw|expire` can refuse, and `decide`
180
+ * cannot emit either of these: the resolution happens before it is called, and
181
+ * a second implementation whose gate emitted one would be describing a
182
+ * different boundary. Both codes are stable public API and both are pinned by
183
+ * `conformance/vectors/refusal-unions.v1.json`.
184
+ */
185
+ export declare const CHANNEL_DECISION_REFUSAL_CODES: readonly [
186
+ /**
187
+ * A sender arrived on a channel the policy maps senders for, and it maps this
188
+ * one to nobody — or the policy could not be loaded at all. Nothing is
189
+ * decided; one `audit.decision_refused` records the observed id.
190
+ */
191
+ "sender-unmapped",
192
+ /**
193
+ * One sender id is claimed by two approvers. `core/policy-load.ts` refuses
194
+ * such a policy outright, so reaching this at decision time means a caller
195
+ * supplied a policy that never passed a load; either way the runtime does not
196
+ * pick.
197
+ */
198
+ "sender-ambiguous",
199
+ /**
200
+ * The policy maps this channel's senders in the KEYED form and no sender key
201
+ * resolves in this process (APRV-370).
202
+ *
203
+ * Its own code rather than a `sender-unmapped`, because the two say opposite
204
+ * things and want opposite repairs. Unmapped says the policy does not name
205
+ * this account: the operator looks at the account and decides whether to add
206
+ * it. This says the runtime could not evaluate the mapping AT ALL, for every
207
+ * account, and the repair is {@link SENDER_KEY_ENV} in the listener's
208
+ * environment. A caller that could not tell them apart would send an operator
209
+ * looking for an intruder when what happened is that a process started
210
+ * without its key.
211
+ *
212
+ * It refuses the WHOLE channel, not only the keyed entries, and that is the
213
+ * strict reading rather than an accident. Without the key the runtime cannot
214
+ * compute any digest, so it cannot check whether the observed account is also
215
+ * claimed by a keyed approver, which means it cannot run the ambiguity check
216
+ * the mapping's safety rests on. Matching a raw entry while half the roster
217
+ * is unreadable would be resolving an ambiguity by not looking at it
218
+ * (SPEC.md §11.1: ambiguity resolves to the stricter path, always). There is
219
+ * deliberately no fallback to the raw comparison.
220
+ */
221
+ "sender-key-unavailable",
222
+ /**
223
+ * An attestation tap that would decide WHO MAY DECIDE, from a phone, under a
224
+ * policy that cannot answer who is tapping.
225
+ *
226
+ * The attestation ceremony is the one act whose subject can be the sender
227
+ * mapping itself, so resolving it against the file being attested would let
228
+ * whoever edited that file name themselves as the approver of their own
229
+ * edit. It is resolved against the policy IN FORCE instead, and this code is
230
+ * what fires when that policy cannot answer: its bytes are not recoverable,
231
+ * or it maps no sender for this channel while the amendment changes the
232
+ * mapping. The repair is a terminal, which supplies no sender and is where a
233
+ * `policy.core` edit happens anyway.
234
+ */
235
+ "attest-requires-terminal"];
236
+ export type ChannelDecisionRefusalCode = (typeof CHANNEL_DECISION_REFUSAL_CODES)[number];
237
+ /** Is `code` one of this union's members? Used where a code arrives as a string. */
238
+ export declare function isChannelDecisionRefusalCode(code: string): code is ChannelDecisionRefusalCode;
239
+ /** The outcome of resolving one observed sender against one policy. */
240
+ export type SenderResolution = {
241
+ /** Nothing to resolve against; the surface keeps its configured actor. */
242
+ kind: "configured";
243
+ /** Why: no sender was observed, or the policy maps none for this channel. */
244
+ reason: "no-sender" | "channel-unmapped";
245
+ } | {
246
+ kind: "mapped";
247
+ approver: string;
248
+ actor: string;
249
+ source: SenderSource;
250
+ /** The sender AS THE RECORD CARRIES IT: raw, or the keyed digest. */
251
+ recorded: RecordedSender;
252
+ } | {
253
+ kind: "unmapped";
254
+ sender: RecordedSender;
255
+ message: string;
256
+ } | {
257
+ kind: "ambiguous";
258
+ sender: RecordedSender;
259
+ approvers: string[];
260
+ message: string;
261
+ } | {
262
+ kind: "key-unavailable";
263
+ sender: ChannelSender;
264
+ message: string;
265
+ };
266
+ /**
267
+ * A sender as a RECORD carries it (APRV-370).
268
+ *
269
+ * Under a raw mapping this is `{channel, id}` and byte-identical to what every
270
+ * build since APRV-324 wrote. Under a keyed mapping `id` is the
271
+ * {@link hashedSenderId} form and `hashed` is `true`.
272
+ *
273
+ * `id` carries the WHOLE `hmac-sha256:<hex>` string rather than the bare hex,
274
+ * deliberately: that is exactly the string the policy carries, so an operator
275
+ * reading a refusal off the log has a line they can paste into `senders`
276
+ * without transforming it, and a reader correlating a log to a policy can grep
277
+ * one for the other. The `hashed` flag is not a second source of truth for the
278
+ * same fact — the schema pins `id` to the digest shape whenever it is present,
279
+ * so the two cannot disagree — it is the field anything machine-readable
280
+ * branches on without parsing a string.
281
+ *
282
+ * `hashed` is `true` or absent, never `false`. A raw record is the record this
283
+ * runtime already wrote, and adding a field to it that says "this is what it
284
+ * always was" would make every pre-APRV-370 record read as though it were
285
+ * missing something.
286
+ */
287
+ export interface RecordedSender {
288
+ channel: string;
289
+ id: string;
290
+ hashed?: true;
291
+ }
292
+ /**
293
+ * Every `(channel, id)` a policy maps, to the approver ids claiming it.
294
+ *
295
+ * The policy is written person to sender, so a reader sees each human's
296
+ * identity in one place; this is the inversion the decision path needs, and it
297
+ * is computed rather than stored so the two cannot disagree. A value with more
298
+ * than one member is the ambiguity {@link checkSenderMappings} refuses at load.
299
+ *
300
+ * Approver ids are sorted, so one policy always produces the same message.
301
+ */
302
+ export declare function senderIndex(approvers: Record<string, {
303
+ channels: string[];
304
+ senders?: Record<string, string>;
305
+ }> | undefined): Map<string, string[]>;
306
+ /**
307
+ * Does this policy map any sender for `channel`?
308
+ *
309
+ * The migration switch of design §6: false is mode 1 for that channel, and the
310
+ * behaviour is exactly the listener identity of every build before this one.
311
+ */
312
+ export declare function mapsSendersFor(approvers: Record<string, {
313
+ channels: string[];
314
+ senders?: Record<string, string>;
315
+ }> | undefined, channel: string): boolean;
316
+ /** Which forms this policy's mapping for `channel` uses (APRV-370). */
317
+ export interface SenderMappingForms {
318
+ /** At least one value is a decimal account id. */
319
+ raw: boolean;
320
+ /** At least one value is a {@link hashedSenderId} digest. */
321
+ keyed: boolean;
322
+ }
323
+ /**
324
+ * Which form, or forms, a policy maps `channel`'s senders in.
325
+ *
326
+ * Both flags can be true: nothing forbids a policy that names one approver
327
+ * raw and another keyed, and this runtime does not refuse one. What it does is
328
+ * refuse every tap on such a channel when the key is missing, because the half
329
+ * it cannot evaluate is still part of the roster it is checking for ambiguity.
330
+ * `approval doctor` reports the mixed state so an operator can finish the
331
+ * migration rather than discover it at a tap.
332
+ */
333
+ export declare function senderMappingForms(approvers: Record<string, {
334
+ channels: string[];
335
+ senders?: Record<string, string>;
336
+ }> | undefined, channel: string): SenderMappingForms;
337
+ /**
338
+ * The load-time check of design §3.2 mode 4: one sender id, at most one person.
339
+ *
340
+ * Returns the message, or `null` when the policy is clear. `core/policy-load.ts`
341
+ * turns a message into a `sender-ambiguous` load failure, which means the
342
+ * policy does not load, which means every class resolves `manual` (SPEC.md
343
+ * §5.2, fail closed). That is the strictest answer available and the only
344
+ * honest one: the file says two people are one account, and there is no reading
345
+ * of it under which a decision by that account names a person.
346
+ */
347
+ export declare function checkSenderMappings(approvers: Record<string, {
348
+ channels: string[];
349
+ senders?: Record<string, string>;
350
+ }> | undefined): string | null;
351
+ /**
352
+ * Resolve one observed sender against the policy in force (design §3.2,
353
+ * APRV-370).
354
+ *
355
+ * Total, pure, and the whole of the decision-time logic. It never reads a file,
356
+ * never reads the log, never reads the environment and never decides anything:
357
+ * the caller (`channels/contract.ts`) turns a `mapped` into the actor it hands
358
+ * the gate, and a refusal into an `audit.decision_refused` and a message to the
359
+ * chat.
360
+ *
361
+ * `sender` absent is mode 1 and the reason the CLI channel and every web post
362
+ * are untouched by this: neither supplies one, so neither can claim anybody.
363
+ *
364
+ * `key` is the operator's sender key, or `null` for none, and it DEFAULTS TO
365
+ * NULL. That default is the fail-closed direction and it is load-bearing: a
366
+ * caller that has not been taught about the key refuses every keyed mapping
367
+ * rather than silently comparing raw ids against digests and finding nothing,
368
+ * which would read exactly like a stranger tapping.
369
+ */
370
+ export declare function resolveSender(load: PolicyLoadResult, sender: ChannelSender | undefined, key?: string | null): SenderResolution;
371
+ /**
372
+ * The sender as a record should carry it (APRV-370).
373
+ *
374
+ * `key` is the key when this channel's mapping is KEYED, and `null` when it is
375
+ * raw. A raw mapping records what it always recorded, which is why a keyed
376
+ * channel is the only thing that changes any existing record's bytes.
377
+ *
378
+ * Note which id is hashed here: the OBSERVED one, from the transport. The
379
+ * digest of an id the policy did not name is exactly the value a refusal wants
380
+ * an operator to see, because it is the line they would paste to map that
381
+ * account, and it discloses the account to nobody who does not already hold the
382
+ * key.
383
+ */
384
+ export declare function recordedSender(sender: ChannelSender, key: string | null): RecordedSender;
385
+ /**
386
+ * The recorded form for a sender on a refusal that never reached
387
+ * {@link resolveSender} (APRV-370).
388
+ *
389
+ * A gesture can be refused before the mapping is consulted at all: the log
390
+ * could not be read, or the policy on disk is not the attested one, so its
391
+ * mapping is not in force. Those records still say who tried, and the question
392
+ * is which FORM that says it in.
393
+ *
394
+ * THE RULE: the form follows the FILE; the mapping follows the ATTESTATION. The
395
+ * form is a disclosure preference the operator wrote down, and honouring it
396
+ * from an unattested file grants nobody anything — the refusal is a refusal
397
+ * either way, and the record names no approver. What must never follow an
398
+ * unattested file is who may decide, and that is decided by
399
+ * {@link resolveSender} against the policy in force, which these paths never
400
+ * reach.
401
+ *
402
+ * A load that failed, or a channel this file maps raw, or no key: the raw id,
403
+ * exactly as every build since APRV-324 recorded it.
404
+ */
405
+ export declare function recordedSenderFor(load: PolicyLoadResult, sender: ChannelSender, key: string | null): RecordedSender;
406
+ /**
407
+ * Every `(channel, id)` pair a policy declares, as a sorted, comparable list.
408
+ *
409
+ * The shape {@link sendersDiffer} compares. Sorted and flattened so that two
410
+ * policies differing only in key order are the same mapping, and a policy whose
411
+ * load failed is `null` rather than an empty mapping — "no senders" and "I
412
+ * could not read the senders" are different answers and only one of them is
413
+ * safe to act on.
414
+ */
415
+ export declare function senderPairs(load: PolicyLoadResult): string[] | null;
416
+ /**
417
+ * Does the amendment change who may be recognized, anywhere, on any channel?
418
+ *
419
+ * The question the attestation rule turns on (§10.3). An amendment that adds,
420
+ * removes or repoints ANY `senders` entry is an amendment about the identity
421
+ * system itself, and the policy in force is the only honest place to ask who
422
+ * may sign for it: asking the proposed file would let whoever wrote it name
423
+ * themselves as the approver of their own edit.
424
+ *
425
+ * `true` when either side could not be read, which is the strict direction: an
426
+ * unreadable mapping is one this runtime cannot prove is unchanged.
427
+ */
428
+ export declare function sendersDiffer(before: PolicyLoadResult, after: PolicyLoadResult): boolean;
429
+ /**
430
+ * The actor a surface records, from the sender it observed and the policy it
431
+ * resolved against — or the refusal that replaces it.
432
+ *
433
+ * One function, every surface (APRV-324 follow-up). Decisions, checkpoint
434
+ * signatures and retrospective reviews all ask the same question of the same
435
+ * two inputs, and three copies of the ladder would be three chances for one of
436
+ * them to keep the pre-mapping behaviour on the gesture that matters most.
437
+ * What differs between the surfaces is what they do with the answer, which is
438
+ * theirs; what must not differ is who the answer names.
439
+ */
440
+ export type SenderActorResolution = {
441
+ ok: true;
442
+ actor: string;
443
+ /**
444
+ * Present only where the actor was RESOLVED from the sender, and in the
445
+ * form the record carries: raw, or the keyed digest (APRV-370).
446
+ */
447
+ sender?: RecordedSender;
448
+ source?: SenderSource;
449
+ } | {
450
+ ok: false;
451
+ code: ChannelDecisionRefusalCode;
452
+ message: string;
453
+ sender: RecordedSender;
454
+ };
455
+ /**
456
+ * Resolve `sender` against `load`, falling back to `configured`.
457
+ *
458
+ * The `configured` actor is what the surface was launched as, and it survives
459
+ * exactly the two modes {@link resolveSender} calls `configured`: no sender
460
+ * observed, and no mapping declared for the channel that observed one.
461
+ *
462
+ * `key` defaults to `null` for the reason {@link resolveSender}'s does: a
463
+ * caller that does not supply one refuses a keyed mapping rather than reading
464
+ * past it.
465
+ */
466
+ export declare function actorForSender(load: PolicyLoadResult, configured: string, sender: ChannelSender | undefined, key?: string | null): SenderActorResolution;
467
+ /**
468
+ * What the person who tapped is told, in one line (APRV-235's rule, applied to
469
+ * these codes).
470
+ *
471
+ * It names the code and says nothing about the mapping: not who is mapped, not
472
+ * how many are, and not the id it saw. The chat is shared, the id belongs in
473
+ * the log where an operator reads it deliberately, and a refusal that recited
474
+ * the roster would turn a mis-tap into a disclosure.
475
+ */
476
+ export declare function senderRefusalLine(code: ChannelDecisionRefusalCode): string;