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,819 @@
1
+ /**
2
+ * `approval codex bridge`: approval.md as the client of Codex's app-server
3
+ * protocol (APRV-361, adopting APRV-349's recommendation).
4
+ *
5
+ * ## Why this exists
6
+ *
7
+ * The native Codex hook refuses every shell call. A `Bash` pre-event carries
8
+ * `tool_input` keys exactly `["command"]`, so the adapter cannot bind the
9
+ * directory the command will run in, and it answers
10
+ * `hook-unsupported-execution-context` rather than approve bytes whose meaning
11
+ * it does not know (APRV-310, APRV-311). The app-server protocol is a different
12
+ * shape: Codex stops before it acts, asks its client, and waits, and the
13
+ * question it asks carries `cwd` on the same frame as the command, minted by
14
+ * the harness runtime rather than reported by the model.
15
+ *
16
+ * `docs/codex-app-server-bridge.md` is the evidence and the recommendation.
17
+ * Read the recommendation before changing anything here: this is an ADVISORY
18
+ * checkpoint for everyday Codex sessions this runtime starts, and it is not a
19
+ * boundary. The auto-reviewer can resolve a question before this client sees
20
+ * it, and the approval policy and sandbox posture decide how many questions
21
+ * exist at all. Both are governed by the harness's own configuration, which
22
+ * this project does not attest. The claim this verb supports is "this client
23
+ * decided every question this app-server child asked in this session", and
24
+ * nothing wider. See the custody section below for which word in that sentence
25
+ * is load-bearing.
26
+ *
27
+ * ## Custody: the server is this process's own child (APRV-365)
28
+ *
29
+ * A pending approval request is replayed to whatever connects NEXT, so "who
30
+ * may connect" is a real question about any app-server, and it is the question
31
+ * the follow-up list filed. For this verb it is answered by construction: the
32
+ * server is started here, by {@link driveSession}, as a child process over
33
+ * stdio pipes. There is no socket, nothing binds a path, and no other process
34
+ * has a file descriptor to speak on. The custody rule is the operating
35
+ * system's rather than this runtime's, which is the strongest kind available
36
+ * and the only kind this project would not have to attest.
37
+ *
38
+ * Two consequences worth stating rather than leaving to be inferred. Within
39
+ * one run there is no replay hazard at all: a question this client is asked
40
+ * cannot reach another client, because there is no other client. And the claim
41
+ * is scoped to THIS CHILD and THIS SESSION: a Codex started outside this
42
+ * arrangement is a different process with a different connection, and nothing
43
+ * here observes it, exactly as `docs/codex-activation.md` says of a Codex
44
+ * started outside the confined session.
45
+ *
46
+ * What this verb deliberately does NOT do is inspect the server command for a
47
+ * shape that would attach to something already running instead of starting a
48
+ * child. That would be a guess at another program's command line, which this
49
+ * repository has no record of, and a check written against a guessed shape
50
+ * finds nothing while reporting that it looked (the trap APRV-379 names and
51
+ * APRV-364's item reader is careful about). The property is stated and true;
52
+ * an operator who passes `-- <something that attaches>` after the separator has
53
+ * left the arrangement this section describes, and the report's claim is then
54
+ * about a session this verb did not establish.
55
+ *
56
+ * ## It reuses the hook's flow; it does not fork it
57
+ *
58
+ * Every decision is `cli/hook.ts`'s {@link decideHarnessCall}: the same
59
+ * classifier over `{command, cwd}`, the same human-only refusal, the same
60
+ * unruled `harness.launch.*` refusal, the same sandbox requirement, the same
61
+ * loop floor and unattended guard, and the same register, request and wait
62
+ * against the verified view. What changes is only where the answer goes: a
63
+ * JSON-RPC reply on the connection instead of a decision object on stdout.
64
+ *
65
+ * The request is translated into the hook's own `HookInput` — tool `Bash`,
66
+ * `tool_input.command` the string the server sent, `cwd` the directory the
67
+ * server named — and nothing else is invented. Both fields come from the
68
+ * server, which is what makes them usable: a `cwd` the model reported would be
69
+ * a self-reported field reducing scrutiny (SPEC §11.1 invariant 4).
70
+ *
71
+ * ## The deadline is the policy's, not a harness ceiling
72
+ *
73
+ * Every hook adapter answers inside a ceiling its harness sets, and the retry
74
+ * grace and adopt-on-retry machinery exist so a denial-by-deadline is
75
+ * recoverable. This transport has no timeout at all (`docs/codex-app-server-bridge.md`,
76
+ * question 2), so the wait here defaults to the policy's `approval_ttl`: a
77
+ * human who answers in eleven minutes is answering rather than arriving too
78
+ * late. `--wait` overrides it; nothing shortens the request's own TTL.
79
+ *
80
+ * ## What it answers, and what it never answers
81
+ *
82
+ * `accept` and `decline` only, in the vocabulary the request advertised through
83
+ * `availableDecisions`, and never `acceptForSession`, `cancel` or `abort`.
84
+ * `acceptForSession` converts one decision into standing authority for a whole
85
+ * session, which is a grant shape this project does not have. `cancel` and
86
+ * `abort` mean "stop the turn", which is a different act from "no to this
87
+ * action", and sending one would record an interruption as a denial.
88
+ *
89
+ * That rule is carried by the TYPE since APRV-367, not by a reviewer's memory:
90
+ * every reply word is a {@link BridgeDecisionWord}, whose eight inhabitants are
91
+ * the four spellings of yes and the four of no, and {@link encodeDecision} is
92
+ * the one place a decision becomes bytes and re-asks the question at runtime. A
93
+ * word outside the vocabulary is unconstructible, and were one to arrive anyway
94
+ * the reply becomes a decline, since the only safe substitute for a word you
95
+ * cannot name is no.
96
+ *
97
+ * ## The two file-change APIs, and the correlation (APRV-363, APRV-379)
98
+ *
99
+ * The LEGACY `applyPatchApproval` carries its `fileChanges` map on the request
100
+ * itself, so there is nothing to correlate and nothing to re-render. Since
101
+ * APRV-363 it goes through the same `decideHarnessCall` the exec half uses,
102
+ * classified by the paths it names and bound with the change as it arrived plus
103
+ * its digest. A request carrying a map and no directory is refused exactly as
104
+ * an exec request with no `cwd` is: a relative path resolves somewhere, and a
105
+ * directory this client guessed would be a guess the grant is bound to.
106
+ *
107
+ * An `item/fileChange/requestApproval` on the ITEM-BASED API carries no
108
+ * content: `threadId`, `turnId`, `itemId`, `startedAtMs`, `reason` and
109
+ * `grantRoot`, and nothing else. The bytes arrived EARLIER, on the
110
+ * `item/started` notification for that item, whose `item.changes` is an array
111
+ * of `{path, kind, diff}` (observed on 0.155.0; the frame is in
112
+ * `docs/codex-app-server-bridge.md`, question 1). Since APRV-379 this verb
113
+ * keeps every item the thread announces, by item id, and answers the request
114
+ * against the frame that id names. What reaches the classifier is the change
115
+ * set the server sent, in the shape it sent it: the paths take their classes
116
+ * and the payload binds the changes verbatim with a digest over them as
117
+ * received. Nothing is re-rendered into an `apply_patch` envelope and nothing
118
+ * parses `diff`.
119
+ *
120
+ * The correlation is where an approval could authorize bytes nobody classified,
121
+ * so every way the two frames could fail to be about one change is a refusal:
122
+ * an id no `item/started` announced, an id naming an item that is not a
123
+ * `fileChange`, a frame with no readable change set, and a request naming a
124
+ * thread or a turn the frame does not, are all `bridge-file-change-unbound`.
125
+ * An item whose `item/completed` arrived BEFORE the question is
126
+ * `bridge-file-change-already-completed`: a change that finished before it was
127
+ * asked about is not a change this client is in a position to decide.
128
+ *
129
+ * A server request this verb does not recognise is declined too, on the same
130
+ * rule: a question nobody classified is not a question to answer yes to.
131
+ *
132
+ * ## The approval policy is pinned, and the pin is checked (APRV-366)
133
+ *
134
+ * The thread starts with `approvalPolicy: "untrusted"`, which is `UnlessTrusted`
135
+ * on the wire and the only variant under which every command and every patch
136
+ * asks. Under `on-request` or `never` an unknown fraction of the session never
137
+ * reaches this client, and "this client decided every question it was asked"
138
+ * would still be true while meaning nothing. There is no flag.
139
+ *
140
+ * What the verb can prove about it depends on the server. A `thread/start` the
141
+ * server refuses stops the run, carrying its error verbatim, which is where a
142
+ * refusal of the value itself lands. A server that reports an effective policy
143
+ * of its own, on `thread/start`'s result or on a thread notification, stops the
144
+ * run when that policy is not the pinned one. A server that reports nothing is
145
+ * run against, and the report then claims only what happened: the pin was
146
+ * requested and no frame confirmed it.
147
+ *
148
+ * ## A probe turn runs first, and it can stop the session (APRV-364)
149
+ *
150
+ * Codex carries a server-side auto-reviewer that can resolve an approval with a
151
+ * model call before this client is asked, and tells the client afterwards
152
+ * through `item/autoApprovalReview` notifications. Whether it runs is a setting
153
+ * in the harness's own configuration, which this project does not attest, and
154
+ * there is no frame in the observed vocabulary where the server reports it. So
155
+ * it cannot be READ, and the only way to establish anything is to watch what
156
+ * happens to one command.
157
+ *
158
+ * Every start therefore runs a PREFLIGHT turn asking for one harmless command
159
+ * ({@link PROBE_COMMAND}) before the operator's own turn, with no flag to skip
160
+ * it. Three outcomes, told apart by the command-item notifications:
161
+ *
162
+ * - an approval request for it reaches this client: the run continues, and the
163
+ * report records the pin as confirmed by OBSERVATION;
164
+ * - a command ran and no request arrived: `bridge-approval-policy-mismatch`,
165
+ * because a policy under which one command did not ask is not `untrusted`
166
+ * whatever the server says about itself;
167
+ * - no command ran at all: `bridge-preflight-void`, carrying the turn's frames
168
+ * verbatim. It is never retried and never reported as a pass.
169
+ *
170
+ * An `item/autoApprovalReview` notification in either turn is
171
+ * `bridge-auto-reviewer-active` and ends the run, after one
172
+ * `audit.question_preempted` is appended for it (APRV-378): the moment
173
+ * something other than this gate answered a question this gate exists to ask is
174
+ * the moment this project most wants in the log.
175
+ *
176
+ * THE PROBE'S OWN REQUEST NEVER REACHES THE GATE. It is declined immediately,
177
+ * as an observation. Routing it through `decideHarnessCall` would register an
178
+ * action and could put `true` on a human's phone at every bridge start, and a
179
+ * preflight that spends a person's attention is not a harmless one.
180
+ *
181
+ * WHAT A PASS MEANS, exactly: one question reached this client unanswered by
182
+ * anything else. It is not a proof that the auto-reviewer is off for every
183
+ * question, and nothing here says that it is.
184
+ *
185
+ * ## One at a time, on purpose
186
+ *
187
+ * The gate's wait is synchronous, so while one question is being decided this
188
+ * process is not reading frames. That is the fail-closed direction: frames
189
+ * queue and are answered in arrival order, and a second request cannot be
190
+ * answered from a decision made about the first. The observed protocol asks one
191
+ * question at a time (the turn does not move past an unanswered one).
192
+ *
193
+ * ## Limitations stated rather than implied
194
+ *
195
+ * An OPEN GATE WINDOW is not honoured here. The hook's bypass prints a hook
196
+ * verdict and appends a record shaped for the hook; wiring it into this
197
+ * transport is more surface than this task carries, and ignoring it is the
198
+ * strict direction — a window widens authority, and this verb simply does not
199
+ * widen. An operator who opens a window and expects this verb to fall through
200
+ * will find it still asking.
201
+ */
202
+ import { hookScope, type HarnessVerdict } from "./hook.js";
203
+ import type { Streams } from "./main.js";
204
+ /**
205
+ * The approval policy this verb starts a thread under (`untrusted` on the
206
+ * wire).
207
+ *
208
+ * `UnlessTrusted` is the only variant under which every command asks
209
+ * (`docs/codex-app-server-bridge.md`, question 5), and `unless-trusted` is
210
+ * REFUSED by the server: the accepted spelling is `untrusted`, established by
211
+ * the 2026-09-18 probe. There is no flag: a session gating an unknown fraction
212
+ * of itself is the thing this pin exists to prevent, and an operator who could
213
+ * pass `on-request` would have exactly that session (APRV-366).
214
+ */
215
+ export declare const APPROVAL_POLICY = "untrusted";
216
+ /** The sandbox posture the thread starts under. */
217
+ export declare const SANDBOX = "read-only";
218
+ /**
219
+ * The decision words this verb will send, most literal first.
220
+ *
221
+ * `accept`/`decline` are the item-based API's spelling and `approved`/`denied`
222
+ * the legacy one. The session-wide and amendment-carrying variants are absent
223
+ * from the accept list, and `cancel`/`abort` from the decline list, for the
224
+ * reasons in this module's header. A value this runtime does not name is never
225
+ * sent, however loudly the server advertises it.
226
+ */
227
+ export declare const ACCEPT_WORDS: readonly ["accept", "approved", "approve", "allow"];
228
+ export declare const DECLINE_WORDS: readonly ["decline", "denied", "deny", "reject"];
229
+ /** One of the four spellings of yes this verb will send. */
230
+ export type BridgeAcceptWord = (typeof ACCEPT_WORDS)[number];
231
+ /** One of the four spellings of no. */
232
+ export type BridgeDeclineWord = (typeof DECLINE_WORDS)[number];
233
+ /**
234
+ * Everything this verb can put in a `decision` field, as a type (APRV-367).
235
+ *
236
+ * The list is the whole vocabulary: eight spellings of two words. A value
237
+ * outside it is a TYPE ERROR at every point the reply is built, which is the
238
+ * half of the rule a reviewer cannot forget to check, and it is refused again
239
+ * at runtime by {@link encodeDecision}, which is the half that survives a
240
+ * caller with an `any` in it.
241
+ */
242
+ export type BridgeDecisionWord = BridgeAcceptWord | BridgeDeclineWord;
243
+ /** The two things this verb ever means, whatever the server calls them. */
244
+ export type BridgeOutcome = "accept" | "decline";
245
+ /** Is this one of the eight words? The runtime face of {@link BridgeDecisionWord}. */
246
+ export declare function isBridgeDecisionWord(value: unknown): value is BridgeDecisionWord;
247
+ /**
248
+ * The reply payload for one word: the ONE place a decision becomes bytes.
249
+ *
250
+ * `null` for anything this runtime does not name. The types make such a value
251
+ * unconstructible, so this is the defence against the code changing out from
252
+ * under the types rather than against any input a server can send: no
253
+ * `availableDecisions` list can reach it, because {@link chooseDecision} only
254
+ * ever returns a member.
255
+ *
256
+ * The caller answers `null` by sending a DECLINE, and that is the whole
257
+ * reasoning: the only safe substitute for a word you cannot name is no. It gets
258
+ * no refusal code of its own, because a code in a closed union that no input
259
+ * can produce is a string a second implementation cannot exercise and would
260
+ * have to take on trust.
261
+ */
262
+ export declare function encodeDecision(word: string): {
263
+ decision: BridgeDecisionWord;
264
+ } | null;
265
+ /**
266
+ * Every refusal this verb can answer with that is NOT a gate verdict, closed
267
+ * and machine-readable (SPEC §11.1 invariant 7).
268
+ *
269
+ * A gate verdict carries the gate's own code (`hook-class-human-only`,
270
+ * `hook-rejected`, `hook-timeout`, and the rest); these are the refusals the
271
+ * bridge reaches on its own, before or instead of asking.
272
+ */
273
+ export declare const BRIDGE_REFUSAL_CODES: readonly [
274
+ /** A file-change request whose content this verb cannot produce (APRV-363). */
275
+ "bridge-file-change-unbound",
276
+ /** A server request this verb has no reading for. */
277
+ "bridge-unknown-request",
278
+ /**
279
+ * A file-change request whose item had already COMPLETED when it arrived
280
+ * (APRV-379).
281
+ *
282
+ * Distinct from `bridge-file-change-unbound`, which says the content could
283
+ * not be produced. Here the content was produced: the `item/started` frame is
284
+ * held, the item id correlates, and the change set is right there. What is
285
+ * wrong is the ORDER. `item/completed` for that item arrived before the
286
+ * question about it did, and a change that finished before it was asked about
287
+ * is not a change this client is in a position to decide. Answering yes would
288
+ * put a grant in the log for an effect that had already happened, and
289
+ * answering the ordinary no would tell an operator to go and stop something
290
+ * that is over.
291
+ *
292
+ * The repairs differ, which is why the codes do: an unbound change is a
293
+ * correlation that did not happen and points at this client or at a protocol
294
+ * that changed shape, and this one points at a session whose approval policy
295
+ * is not the one it was pinned to, or at a server that reordered its frames.
296
+ */
297
+ "bridge-file-change-already-completed",
298
+ /**
299
+ * An exec request whose command string names no argv this client can bind
300
+ * (APRV-362).
301
+ *
302
+ * Distinct from `bridge-request-unbound`, which says a field is MISSING. This
303
+ * one says the field arrived and could not be read as the rendering of an
304
+ * argv: an unterminated quote, a bare double quote, a trailing backslash, or
305
+ * whitespace no join produces. The repairs differ, which is why the codes do:
306
+ * a missing `cwd` is a server that changed shape, and this is a command
307
+ * string that did not come from joining the words that will run.
308
+ */
309
+ "bridge-command-unbound",
310
+ /** An exec request carrying no command string, or no cwd. */
311
+ "bridge-request-unbound"];
312
+ export type BridgeRefusalCode = (typeof BRIDGE_REFUSAL_CODES)[number];
313
+ /**
314
+ * Every way this verb STOPS a session instead of answering a question
315
+ * (APRV-366), closed and machine-readable.
316
+ *
317
+ * A separate array from {@link BRIDGE_REFUSAL_CODES}, and deliberately not a
318
+ * member of it. Those are answers: one approval request declined, the turn
319
+ * carrying on. These end the run before or instead of a turn, because the
320
+ * session could not be established as the kind of session this verb is willing
321
+ * to sit in front of. The conformance union `bridge_refusal_codes` is
322
+ * documented as "every way the bridge can decline an app-server approval
323
+ * request", so a stop code inside it would describe a different boundary, which
324
+ * is the reasoning that kept these out of `hook_deny_codes` too.
325
+ *
326
+ * The process exit for both is {@link EXIT_IO}, as it is for every other
327
+ * protocol stop here: the exit codes are frozen public API and a session that
328
+ * could not be started is not a new number. The code below is the distinct part
329
+ * a caller branches on.
330
+ */
331
+ export declare const BRIDGE_STOP_CODES: readonly [
332
+ /**
333
+ * The server refused `thread/start`, so no thread exists and the approval
334
+ * policy this verb requires was never established. The server's own error is
335
+ * carried verbatim in the detail, which is where a refusal of the policy
336
+ * VALUE shows up (the 2026-09-18 probe's `unknown variant \`unless-trusted\`,
337
+ * expected one of \`untrusted\`, \`on-request\`, \`granular\`, \`never\``).
338
+ */
339
+ "bridge-thread-start-refused",
340
+ /**
341
+ * The server started a thread and reported an effective approval policy that
342
+ * is not {@link APPROVAL_POLICY}. Under any other variant an unknown fraction
343
+ * of the session never produces a question at all, so "this client decided
344
+ * every question it was asked" would be true and would mean nothing.
345
+ */
346
+ "bridge-approval-policy-mismatch",
347
+ /**
348
+ * An `item/autoApprovalReview` notification arrived, in the preflight turn or
349
+ * in the real one (APRV-364).
350
+ *
351
+ * Codex carries a server-side auto-reviewer that can resolve an approval with
352
+ * a model call BEFORE the client path runs, and tells the client afterwards
353
+ * through these notifications (`docs/codex-app-server-bridge.md`, question
354
+ * 3). A session with a reviewer in front of the gate is a session whose
355
+ * silence means nothing: the questions this client was not asked are
356
+ * indistinguishable from questions nobody wanted to ask. So the run stops
357
+ * rather than gating whatever is left over.
358
+ *
359
+ * It leaves a RECORD since APRV-378: one `audit.question_preempted`,
360
+ * appended through the real append path before the stop, naming the source,
361
+ * the question as Codex identified it, and the verdict the reviewer reached
362
+ * where the notification stated one. The write is best-effort and the stop
363
+ * does not depend on it; a failure to append is reported on stderr beside
364
+ * the stop rather than swallowed.
365
+ */
366
+ "bridge-auto-reviewer-active",
367
+ /**
368
+ * The preflight turn ran no command at all, so the probe established nothing
369
+ * (APRV-364).
370
+ *
371
+ * The preflight is a prompt, and a model is free to answer a prompt in prose.
372
+ * When that happens no approval request arrives AND no command executes, and
373
+ * the fact AC1 wants — that a command reached this client as a question —
374
+ * was not observed. Reporting it as a pass would be reporting a verdict
375
+ * nobody established, which is the APRV-359 lesson; reporting it as the
376
+ * policy mismatch would blame a healthy session for a model's choice of
377
+ * words. So it is its own code, the report carries the turn's frames
378
+ * verbatim, and nothing is retried: an operator runs the verb again.
379
+ */
380
+ "bridge-preflight-void"];
381
+ export type BridgeStopCode = (typeof BRIDGE_STOP_CODES)[number];
382
+ /**
383
+ * Where the report's claim about the approval policy COMES FROM (APRV-364).
384
+ *
385
+ * APRV-366 wrote this as a boolean, and a boolean could say only that some
386
+ * frame echoed the pin back. The preflight probe establishes the same thing a
387
+ * different way, by watching what happens to one command, and the two are not
388
+ * the same strength of evidence: an echo is the server describing itself, and
389
+ * an observation is a thing that happened. A reader who is told `true` cannot
390
+ * tell them apart, so the field names its source instead.
391
+ *
392
+ * - `unconfirmed` — nothing has confirmed the pin. Where every run starts, and
393
+ * where a run that stopped before the probe finished stays.
394
+ * - `reported` — a server frame named the pinned policy as the effective one.
395
+ * The observed 0.155.0 server names none, so this is rare in practice.
396
+ * - `observed` — a probe command produced an approval request that reached
397
+ * this client. THE HONESTY LINE, and it is narrow on purpose: it proves that
398
+ * ONE question reached this client unanswered by anything else. It is not a
399
+ * proof that the auto-reviewer is off, and no code or document here may say
400
+ * that it is.
401
+ */
402
+ export declare const BRIDGE_PIN_SOURCES: readonly ["unconfirmed", "reported", "observed"];
403
+ export type BridgePinSource = (typeof BRIDGE_PIN_SOURCES)[number];
404
+ /**
405
+ * The thread this verb started, as the report records it (APRV-366).
406
+ *
407
+ * `requested` is what went on the wire, `effective` is what the server said
408
+ * about it, and `confirmed` is the difference between the two: a server that
409
+ * echoes the policy back proves the pin, and one that says nothing leaves this
410
+ * client able to claim only that it asked. That distinction is recorded rather
411
+ * than smoothed over, because a report that said "untrusted" for both cases
412
+ * would be asserting something no frame carried.
413
+ *
414
+ * `confirmed` widened from a boolean to a {@link BridgePinSource} in APRV-364,
415
+ * when the probe gave it a second and stronger way to be true.
416
+ */
417
+ export interface BridgeThreadRecord {
418
+ id: string | null;
419
+ cwd: string;
420
+ requested: {
421
+ approvalPolicy: string;
422
+ sandbox: string;
423
+ };
424
+ effective: {
425
+ approvalPolicy: string | null;
426
+ };
427
+ confirmed: BridgePinSource;
428
+ }
429
+ /**
430
+ * The one command the preflight turn asks for (APRV-364).
431
+ *
432
+ * Chosen for having no effect: it writes nothing, reads nothing, prints
433
+ * nothing, and exits zero. The point of the probe is the QUESTION it raises,
434
+ * and a probe whose command mattered would be a probe an operator had to think
435
+ * about before running.
436
+ */
437
+ export declare const PROBE_COMMAND = "true";
438
+ /**
439
+ * The preflight prompt, written to leave a model as little room as a prompt can
440
+ * (APRV-364).
441
+ *
442
+ * It cannot leave none, which is why {@link BRIDGE_STOP_CODES} carries
443
+ * `bridge-preflight-void`: a model that answers in prose has run no command,
444
+ * and that outcome is reported rather than guessed at.
445
+ */
446
+ export declare const PROBE_PROMPT: string;
447
+ /** What the preflight turn established, once it ended. */
448
+ export declare const BRIDGE_PROBE_OUTCOMES: readonly ["pending", "asked", "executed", "void"];
449
+ export type BridgeProbeOutcome = (typeof BRIDGE_PROBE_OUTCOMES)[number];
450
+ /**
451
+ * The preflight turn, as the report records it (APRV-364).
452
+ *
453
+ * `outcome` is the whole of what the probe established:
454
+ *
455
+ * - `asked` — an approval request for the probe arrived, so one question
456
+ * reached this client. The session continues.
457
+ * - `executed` — a command ran and no request arrived. A policy under which
458
+ * one command did not ask is not `untrusted`, whatever the server said about
459
+ * itself, so the run stops under `bridge-approval-policy-mismatch`.
460
+ * - `void` — no command ran at all, so nothing was established. The run stops
461
+ * under `bridge-preflight-void` and `frames` carries the turn verbatim.
462
+ * - `pending` — the turn has not ended. Only ever seen in a report that
463
+ * stopped for some other reason first.
464
+ */
465
+ export interface BridgePreflightRecord {
466
+ turnId: string | null;
467
+ /** The command the prompt named: {@link PROBE_COMMAND}. */
468
+ command: string;
469
+ outcome: BridgeProbeOutcome;
470
+ /**
471
+ * The word sent on the probe's own approval request, when one arrived.
472
+ *
473
+ * Always a decline. The probe is an observation, and a probe this client
474
+ * approved would be a probe that ran.
475
+ */
476
+ decision: BridgeDecisionWord | null;
477
+ /**
478
+ * Every frame the preflight turn produced, verbatim, present ONLY on the
479
+ * void stop.
480
+ *
481
+ * On a void there is nothing else to show: the stop says a fact could not be
482
+ * established, and the frames are the whole of the evidence for why. On any
483
+ * other outcome they are noise, and a report that always carried them would
484
+ * bury the line that matters.
485
+ */
486
+ frames?: unknown[];
487
+ /** The turn's own error, when `turn/failed` ended it. */
488
+ error?: unknown;
489
+ }
490
+ /**
491
+ * The effective approval policy a server frame reports, or `null` when it
492
+ * reports none.
493
+ *
494
+ * The locations are a documented short list rather than a generic walk: this
495
+ * value can STOP a session, so it is read from places whose meaning is known,
496
+ * and a stray `approvalPolicy` nested inside some unrelated structure must not
497
+ * be able to end a run. The observed 0.155.0 server echoes none of them, which
498
+ * is why an absent value is not itself a stop.
499
+ */
500
+ export declare function effectiveApprovalPolicy(value: unknown): string | null;
501
+ /** One answered question, for the report and for the tests. */
502
+ export interface BridgeAnswer {
503
+ method: string;
504
+ /** The server's own id for the request, echoed on the reply. */
505
+ id: unknown;
506
+ /** `accept` or `decline`, as this verb decided it. */
507
+ outcome: BridgeOutcome;
508
+ /**
509
+ * The word actually sent: one of the eight this runtime names, chosen to
510
+ * match what the request advertised (APRV-367). Its TYPE is the vocabulary,
511
+ * so a row saying `acceptForSession` cannot be constructed here.
512
+ */
513
+ decision: BridgeDecisionWord;
514
+ /** Where that word came from: the request's own list, or this verb's fallback. */
515
+ decisionSource: "advertised" | "fallback";
516
+ /** The gate's code, or a {@link BRIDGE_REFUSAL_CODES} entry. `null` on an accept. */
517
+ code: string | null;
518
+ /** The gate's reason or the refusal's detail, for the operator. */
519
+ detail: string;
520
+ }
521
+ /**
522
+ * The decision words a request says are legal, if it says.
523
+ *
524
+ * Walked generically rather than read from one key path, so a renamed field of
525
+ * the same shape still answers. This is the server describing its own
526
+ * vocabulary, which is the one thing a client should never pin.
527
+ */
528
+ export declare function advertisedDecisions(params: unknown): string[];
529
+ /**
530
+ * The word to send for this outcome, and where it came from (AC4).
531
+ *
532
+ * An advertised word wins, matched case-insensitively and NEVER by prefix, so a
533
+ * server that offers `acceptWithExecpolicyAmendment` is not read as offering
534
+ * `accept`: an amendment carries terms nobody approved. A request advertising
535
+ * nothing gets this verb's own first word, and the report says `fallback` so
536
+ * the choice is visible rather than assumed.
537
+ *
538
+ * What it returns is this runtime's own spelling of the matched word rather
539
+ * than the server's (APRV-367). That is the point of the type: a
540
+ * {@link BridgeDecisionWord} has eight inhabitants, all of them named here, so
541
+ * no path through this function can produce a word this project did not choose
542
+ * to be able to send. The two spellings differ only in letter case, since the
543
+ * match is case-insensitive equality with one of the eight.
544
+ */
545
+ export declare function chooseDecision(params: unknown, outcome: BridgeOutcome): {
546
+ decision: BridgeDecisionWord;
547
+ decisionSource: "advertised" | "fallback";
548
+ };
549
+ /**
550
+ * The exec request's command, as BOTH the words that will run and the string
551
+ * that renders them (APRV-362).
552
+ *
553
+ * ## Which API, and why the string has to be un-joined
554
+ *
555
+ * The two live shapes differ in the one way that matters. The legacy
556
+ * `execCommandApproval` sends `command` as an argv array, which is what the
557
+ * kernel receives. The item-based `item/commandExecution/requestApproval` sends
558
+ * it as a single string, produced by `shlex_join` over that same argv
559
+ * (`docs/codex-app-server-bridge.md`, question 1). This verb drives the
560
+ * ITEM-BASED API, so the string is what it must consume, and the decision the
561
+ * task asked for is settled by which API the bridge speaks rather than by
562
+ * preference: the legacy array is still read, because a request in that shape
563
+ * is a request this client can answer, but it is not the path in use.
564
+ *
565
+ * Consuming the string means un-joining it. A classifier handed the rendering
566
+ * and never the words is classifying its own re-parse, and the gap between the
567
+ * two is where an approval could authorize words nobody read. So both are
568
+ * produced here, both reach the registered payload, and a reader can see the
569
+ * one against the other instead of being asked to trust that they agree.
570
+ *
571
+ * ## What this refuses, and what it deliberately does not
572
+ *
573
+ * A string that is not readable as a join — an unterminated quote, a bare
574
+ * double quote, a trailing backslash, or separation no join emits — is refused
575
+ * with `bridge-command-unbound`. So is an argv this runtime cannot render and
576
+ * read back unchanged, which is unreachable for a correct {@link shlexJoin} and
577
+ * checked anyway, because the cost is one pass over a short string and the
578
+ * failure it guards against is binding words nobody will run.
579
+ *
580
+ * It does NOT demand that {@link shlexJoin} reproduce the received bytes. That
581
+ * would pin the counterpart's quoting predicate, which this repository has no
582
+ * record of: the probe captured one command string and it is consistent with
583
+ * every candidate. A join written for shell safety quotes more than this one
584
+ * does, so demanding byte equality would refuse ordinary traffic on a guess.
585
+ * What is demanded instead is the part that is checkable without knowing which
586
+ * characters the counterpart chose to quote, and the rest is recorded.
587
+ *
588
+ * `proposedExecpolicyAmendment` is deliberately not read, though the task names
589
+ * it as a candidate second source. The 2026-09-18 observation records that the
590
+ * field was PRESENT and records nothing about its shape, and a comparison
591
+ * written against a guessed shape silently matches nothing, which is worse than
592
+ * the check it pretends to be. It becomes usable once a probe captures it.
593
+ */
594
+ export type BoundCommand = {
595
+ ok: true;
596
+ command: string;
597
+ argv: string[];
598
+ source: "rendering" | "argv";
599
+ } | {
600
+ ok: false;
601
+ reason: string;
602
+ };
603
+ export declare function bindCommand(params: unknown): BoundCommand | null;
604
+ /**
605
+ * The turn a frame belongs to, where it names one (APRV-364).
606
+ *
607
+ * The preflight and the real turn are told apart by this value, and a frame
608
+ * that names no turn is decided by which turn is running instead. Both
609
+ * spellings are read because the protocol has used both casings elsewhere and
610
+ * neither reading can widen anything: a turn id is only ever used to decide
611
+ * which of two phases a frame belongs to.
612
+ */
613
+ export declare function turnIdOf(params: unknown): string | null;
614
+ /**
615
+ * Is this method one of Codex's auto-approval-review notifications (APRV-364)?
616
+ *
617
+ * The recorded names are `item/autoApprovalReview/started` and
618
+ * `item/autoApprovalReview/completed`
619
+ * (`docs/codex-app-server-bridge.md`, question 3). The match is on the
620
+ * SUBSTRING rather than on those two exact names, case-folded, because a
621
+ * reviewer notification this runtime failed to recognise would be a session
622
+ * that ran with a reviewer in front of the gate: over-matching costs a stop
623
+ * that an operator can read and re-run, and under-matching costs the whole
624
+ * point of the check.
625
+ */
626
+ export declare function isAutoReviewNotification(method: string): boolean;
627
+ /**
628
+ * The verdict an auto-review notification states, or `null` (APRV-378).
629
+ *
630
+ * Read from a short list of named places rather than by a generic walk, for the
631
+ * reason {@link effectiveApprovalPolicy} is: this value goes into the log as
632
+ * another party's decision, and a string picked up from some unrelated
633
+ * structure would be this runtime putting words in their mouth. `null` is
634
+ * recorded as an ABSENT verdict, never as a default one.
635
+ */
636
+ export declare function autoReviewVerdict(params: unknown): string | null;
637
+ /**
638
+ * Does this notification say a COMMAND was executed (APRV-364)?
639
+ *
640
+ * The one reader in this file written against a shape nobody recorded in full.
641
+ * The 2026-09-18 vocabulary carries `item/started` and `item/completed`, and it
642
+ * does not record the item object they carry, so this looks for an item whose
643
+ * type reads as a command execution, or, failing that, for an item carrying a
644
+ * `command` string.
645
+ *
646
+ * That is a guess, and the reason it is an acceptable one is the direction it
647
+ * fails in. This value only ever chooses BETWEEN TWO STOPS: a preflight turn
648
+ * where a command ran without asking stops under
649
+ * `bridge-approval-policy-mismatch`, and one where nothing ran stops under
650
+ * `bridge-preflight-void`. A guess that misses turns the first into the
651
+ * second; it can never turn either into a pass, because a pass needs an
652
+ * approval request to have ARRIVED, which is a frame this client was handed
653
+ * rather than one it went looking for. The void report carries the frames
654
+ * verbatim, which is also how the real item shape gets recorded here at last.
655
+ */
656
+ export declare function namesCommandExecution(params: unknown): boolean;
657
+ /** What the verb was asked to do, once the flags are read. */
658
+ interface BridgePlan {
659
+ logPath: string;
660
+ root: string;
661
+ options: ReturnType<typeof hookScope>["options"];
662
+ actor: string;
663
+ workspace: string;
664
+ prompt: string;
665
+ waitMs: number;
666
+ intervalMs: number;
667
+ serverCommand: string;
668
+ serverArgs: string[];
669
+ json: boolean;
670
+ }
671
+ /**
672
+ * Decide one exec approval request through the gate (AC2, AC3).
673
+ *
674
+ * Exported for the tests, which drive it without a server so the DECISION can
675
+ * be asserted apart from the transport.
676
+ */
677
+ export declare function decideExecRequest(streams: Streams, plan: BridgePlan, params: unknown): {
678
+ verdict: HarnessVerdict;
679
+ threadId: string | null;
680
+ };
681
+ /**
682
+ * One item this thread's server has told this client about (APRV-379).
683
+ *
684
+ * Every `item/started` is recorded, whatever its type, and not only the
685
+ * file-change ones. That is what lets "an id this client never saw" be told
686
+ * from "an id that names a `userMessage`": the first is a client that missed a
687
+ * frame and the second is a server request that points at the wrong thing, and
688
+ * an operator reading one refusal should not have to guess which happened.
689
+ */
690
+ export interface RecordedItem {
691
+ id: string;
692
+ /** `item.type` as the server spelled it, or `null` where it named none. */
693
+ type: string | null;
694
+ /** The change set VERBATIM, for a `fileChange` item that carried one. */
695
+ changes: readonly unknown[] | null;
696
+ threadId: string | null;
697
+ turnId: string | null;
698
+ /** Has `item/completed` for this item already arrived? */
699
+ completed: boolean;
700
+ }
701
+ /**
702
+ * Every item this thread has announced, by item id.
703
+ *
704
+ * Kept for the LIFE OF THE THREAD rather than cleared at each turn's end,
705
+ * because the frame that carries the content and the request that asks about it
706
+ * are two frames and nothing in the protocol promises they share a turn. It
707
+ * grows with the number of items a session produces, which is the session's own
708
+ * size; a bridge that dropped entries to stay small would be a bridge that
709
+ * refuses a change it was told about, and the refusal would look exactly like a
710
+ * protocol it had not caught up with.
711
+ */
712
+ export type ItemIndex = Map<string, RecordedItem>;
713
+ /**
714
+ * The change set an `item/started` frame carries for a `fileChange` item, or
715
+ * `null` (APRV-379).
716
+ *
717
+ * The observed shape is an ARRAY of `{path, kind, diff}` under `item.changes`
718
+ * (`docs/codex-app-server-bridge.md`, question 1). It is read as an array and
719
+ * carried whole; nothing here looks inside an entry, because the classifier
720
+ * reads the paths and the payload binds the bytes, and a second reader of the
721
+ * same material in this module would be a second account of one change.
722
+ */
723
+ export declare function itemChanges(item: Record<string, unknown>): readonly unknown[] | null;
724
+ /**
725
+ * Record what an `item/started` or `item/completed` notification says
726
+ * (APRV-379).
727
+ *
728
+ * `item/started` writes the entry, `item/completed` marks it completed and
729
+ * leaves the recorded content ALONE. Refreshing the change set from the
730
+ * completion frame would let a server hand this client one change set, be asked
731
+ * about it, and have a different one in the record afterwards; the frame this
732
+ * client decides against is the one it was holding when the question arrived.
733
+ */
734
+ export declare function recordItemFrame(index: ItemIndex, method: string, params: unknown): void;
735
+ /** What the correlation produced, or why it produced nothing. */
736
+ export type Correlation = {
737
+ ok: true;
738
+ item: RecordedItem;
739
+ changes: readonly unknown[];
740
+ } | {
741
+ ok: false;
742
+ code: BridgeRefusalCode;
743
+ detail: string;
744
+ };
745
+ /**
746
+ * Find the `item/started` frame an item-based file-change request refers to
747
+ * (APRV-379).
748
+ *
749
+ * THE WHOLE RISK OF THIS TASK LIVES HERE. The request names an identifier, the
750
+ * bytes arrived on another frame, and a correlation that matched the wrong item
751
+ * would let a grant authorize bytes nobody classified. So every way the two
752
+ * frames could fail to be about the same change is a refusal, and none of them
753
+ * is resolved in favour of going ahead:
754
+ *
755
+ * - no `item/started` for that id was ever seen: `bridge-file-change-unbound`,
756
+ * which is the refusal this verb has answered since APRV-361;
757
+ * - the id names an item that is not a `fileChange`: same code, its own detail.
758
+ * An id that points at a `userMessage` is a request this client has no
759
+ * content for, however much content that item has;
760
+ * - the item carried no readable change set: same code. A `fileChange` frame
761
+ * with nothing in `changes` is an identifier again;
762
+ * - the request names a THREAD or a TURN the frame does not: same code.
763
+ * Item ids are server-minted and observed unique, so this should never fire,
764
+ * and that is exactly why it is checked rather than assumed. Two frames that
765
+ * disagree about which conversation they belong to are not established to be
766
+ * about one change, and "should never happen" is the reasoning that lets a
767
+ * wrong match through. A frame or a request that names NEITHER field is not
768
+ * held to it: absence is not disagreement, and the observed frames carry
769
+ * both;
770
+ * - the item already COMPLETED: `bridge-file-change-already-completed`, for the
771
+ * reasons that code carries.
772
+ */
773
+ export declare function correlateFileChange(index: ItemIndex, params: unknown): Correlation;
774
+ /**
775
+ * The change map a file-change request carries INLINE, or `null` (APRV-363).
776
+ *
777
+ * The LEGACY `applyPatchApproval` carries `fileChanges`, a map of path to
778
+ * change, on the request itself. There is nothing to correlate and nothing
779
+ * arrives on another frame, so it is the one file-change shape this verb can
780
+ * bind: the bytes it decides about are the bytes it was sent.
781
+ */
782
+ export declare function inlineFileChanges(params: unknown): Record<string, unknown> | null;
783
+ /**
784
+ * Decide one file-change request, whichever API it arrived on (APRV-363,
785
+ * APRV-379).
786
+ *
787
+ * Through the SAME path an exec request takes: the hook's `decideHarnessCall`,
788
+ * so the human-only refusal, the loop floor, the unattended guard, the
789
+ * registration and the wait are one implementation. What differs is the tool
790
+ * name and the bound material, and the description of both is `cli/hook.ts`'s,
791
+ * never this module's.
792
+ *
793
+ * TWO SOURCES FOR THE CHANGE SET, one decision path. The LEGACY
794
+ * `applyPatchApproval` carries it inline, so it is read off the request. The
795
+ * ITEM-BASED `item/fileChange/requestApproval` carries an identifier, so it is
796
+ * correlated to the `item/started` frame this client recorded, by
797
+ * {@link correlateFileChange}, which refuses rather than guessing. Either way
798
+ * what reaches the describer is the change set the server sent, in the shape it
799
+ * sent it.
800
+ *
801
+ * THE DIRECTORY, and the two halves differ here for a recorded reason. The
802
+ * legacy request carries one (`cwd`, or `grantRoot` for the same purpose) and
803
+ * is refused without it, exactly as APRV-363 left it. The item-based request
804
+ * carries neither: `grantRoot` was `null` in both captures and there is no
805
+ * `cwd` on that shape at all (`docs/codex-app-server-bridge.md`, question 1).
806
+ * So it falls back to the workspace THIS CLIENT named on `thread/start`, which
807
+ * is this client's own binding rather than a guess or a server claim, and the
808
+ * fallback widens nothing: the observed change paths are absolute, the
809
+ * describer resolves every path against that directory, and one landing
810
+ * outside it is refused `hook-io` whichever way it was spelled.
811
+ */
812
+ export declare function decideFileChangeRequest(streams: Streams, plan: BridgePlan, params: unknown, items: ItemIndex): {
813
+ verdict: HarnessVerdict;
814
+ threadId: string | null;
815
+ };
816
+ export declare function runCodexBridge(argv: string[], streams: Streams, cwd: string): Promise<number>;
817
+ /** The help text, printed by `approval codex bridge --help`. */
818
+ export declare const CODEX_BRIDGE_HELP: string;
819
+ export {};