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
@@ -26,5 +26,78 @@ both installed runtime versions unchecked.
26
26
  The package does not run a postinstall script, call sudo, edit Codex
27
27
  configuration, create accounts, load services, access credentials, start a
28
28
  model, or alter APPROVAL.md. A human or MDM system must eventually install and
29
- own the reviewed artifacts. APRV-325.2 and APRV-325.3 must provide the broker,
30
- runner and end-to-end denial evidence before the session can be called enforced.
29
+ own the reviewed artifacts.
30
+
31
+ ## The broker has landed; the runner has not (APRV-325.2)
32
+
33
+ `approval codex apply` and the one-tool server `approval codex serve` now exist:
34
+ a bounded create/replace/delete/move proposal against the manifest's workspace,
35
+ one registered action per distinct path class, authorized through the real gate,
36
+ every leg started before any byte moves, then staged, journaled and applied
37
+ under a workspace lock, with the outcome taken from reading the workspace back.
38
+ `docs/codex-workspace-broker.md` is its reference.
39
+
40
+ That is a gate on workspace writes and nothing more. On its own a door beside an
41
+ open window is decoration, which is what APRV-325.3 below closes.
42
+
43
+ ## The confined session (APRV-325.3)
44
+
45
+ `approval codex start --manifest <abs>` prepares the room a session's shell runs
46
+ in, and `-- <command>` runs something inside it. Five things the shell does not
47
+ get, and where each is enforced:
48
+
49
+ | withheld | enforced by |
50
+ |---|---|
51
+ | canonical workspace writes | Seatbelt `(deny file-write*)` with an allow-list naming only the disposable workspace |
52
+ | gate writes (log, policy, vault, keys) | the same deny; the gate home is not on the allow-list |
53
+ | reads of anything but the two workspaces | Seatbelt `(deny file-read*)` with an allow-list of two roots (APRV-347's read jail) |
54
+ | ambient credentials | an environment ALLOW-list (`PATH`, `HOME`, `SHELL`, `USER`, `LANG`, `TERM` and a few more) applied after `core/child-env.ts`'s credential strip |
55
+ | credential material on disk | Seatbelt `denyRead` over the vault, the environment map and the sealing keys, emitted after the jail's allows so it is the last word |
56
+ | external egress | Seatbelt `(deny network-outbound)`, loopback included |
57
+ | mutable executor code | the broker, the CLI and the pinned Node live under the root-owned install root, which is on neither allow-list |
58
+
59
+ The read jail has exactly two roots, the disposable workspace and the canonical
60
+ workspace, and `core/sandbox.ts` adds the fixed runtime set and the running
61
+ command's own install prefix itself. A session has to READ the tree it is
62
+ reasoning about, which is why the canonical workspace is a root and why write
63
+ confinement rather than read denial is what stops it changing one. Everything
64
+ the operator's home holds beside it — other repositories, `~/.ssh`, the gate
65
+ home — is outside the jail and unreadable, and that is the half `denyRead` alone
66
+ could never cover: a deny-list has to have heard of a path to deny it.
67
+
68
+ Two properties are worth stating rather than implying. **There is no opt-out.**
69
+ `approval run` has `--no-sandbox`, because an operator holding a human's grant
70
+ may deliberately reach the world; a confined session has no such flag and no
71
+ unwrapped fallback, and a host with no sandbox mechanism REFUSES
72
+ (`sandbox-unsupported`) where `approval run` would record `unsupported` and
73
+ proceed. A session advertised as confined and not confined is worse than no
74
+ session. And **descendants are confined too**: Codex does not write files by
75
+ calling into this runtime, it spawns shells that do, so the proof that matters
76
+ is the one where the confined child's own `/bin/sh` grandchild is denied.
77
+
78
+ ### Activation and rollback
79
+
80
+ The operator runbook lives in `docs/codex-activation.md`, under **Broker session
81
+ activation (Lane 4a)**: the ordered commands, which steps are human-only, and
82
+ how to roll back. That file also holds the native hook's trust ceremony, and the
83
+ two are deliberately separate claims — a green trust ceremony with no broker
84
+ enforces nothing, and a working broker with no Telegram proves nothing about
85
+ whether a decision can reach a human.
86
+
87
+ ### What is still not proven
88
+
89
+ The confinement evidence covers processes this runtime spawns and their
90
+ descendants, on macOS. It is not a claim about a Codex desktop application a
91
+ person starts outside `codex start`: that process is not in the room, and what
92
+ was measured about it is in `docs/codex-boundary-probe.md`. Linux has no
93
+ mechanism in this build (`core/sandbox.ts`'s stated gap), so a Linux host
94
+ refuses rather than running unconfined. Inbound sockets are not denied. The
95
+ environment allow-list is a control over NAMES and therefore best-effort by
96
+ construction; the load-bearing control beside it is that egress is denied, so a
97
+ secret that does reach the child has nowhere to go.
98
+
99
+ The read jail is a control over PATHS and has the same shape of limit: a secret
100
+ someone checked into the canonical workspace is inside a root the session may
101
+ read, and no sandbox rule will change that. What the jail buys is that
102
+ everything outside those two roots is unreadable whether or not anyone thought
103
+ to name it.
@@ -0,0 +1,118 @@
1
+ # The Codex workspace broker
2
+
3
+ `approval codex apply`, and the one-tool server `approval codex serve` in front
4
+ of it, are the executable half of APRV-325. `docs/codex-workspace-planner.md`
5
+ describes the read-only planner this is built on; that document's closing
6
+ warning, that snapshot checks establish no OS custody, is the gap this one
7
+ closes as far as a POSIX host allows and says plainly where it does not.
8
+
9
+ ## What is fixed, and what a caller may say
10
+
11
+ Everything that decides authority comes from the instance manifest, which lives
12
+ under a root-owned install root (`docs/codex-enforced-session.md`,
13
+ `approval codex doctor --strict`):
14
+
15
+ | fact | source |
16
+ |---|---|
17
+ | acting identity | `agent:codex-<instance_id>` from the manifest |
18
+ | workspace root | `paths.workspace` |
19
+ | policy file | `paths.policy` |
20
+ | event log | `paths.log` |
21
+ | action class | derived from the path by the policy's `protected_paths` |
22
+ | reversibility | always `false` |
23
+
24
+ A caller supplies two things and nothing else: `operations`, and
25
+ `expected_policy_sha256`. Any other key is refused `input-invalid`. There is no
26
+ `--as`, no path, no class, no token and no sandbox argument in the published
27
+ tool schema, because none was ever published and so none had to be removed.
28
+
29
+ The server-side allowlist is positive and has one member. A name not on it is
30
+ refused whether or not anything advertised it, and the check runs at call time
31
+ as well as at list time.
32
+
33
+ ## The order of one apply
34
+
35
+ 1. **Verified log, attested policy.** The log is read under chain verification;
36
+ the policy file is read once and those exact bytes are hashed and checked
37
+ against the latest attestation. Unattested, drifted or unparseable policy
38
+ refuses before a plan exists.
39
+ 2. **Expected digest.** A caller's `expected_policy_sha256` that differs from
40
+ the attested digest refuses `attestation-drift`: the proposal was built
41
+ against a policy nobody is enforcing.
42
+ 3. **Plan.** `planWorkspaceProposal` does the path work. Traversal, absolute
43
+ paths, symlinks, hardlinked preimages, case aliases, missing sources,
44
+ existing destinations and preimage mismatches are refused, and a human-only
45
+ class is refused *before* its preimage is read.
46
+ 4. **Register.** One action per distinct path class, under a task id derived
47
+ from the plan's payload hash. Classes are never collapsed: the class is what
48
+ the operator's roster, budget and autonomy are keyed to, and an action that
49
+ reports a cheaper class than it performs is exactly the self-report SPEC.md
50
+ §11.1 invariant 4 forbids.
51
+ 5. **Authorize every leg.** Policy decides. A leg the policy sends to a human
52
+ refuses `approval-required` and names the action keys awaiting a decision;
53
+ nothing is written, and the identical proposal applies once a person has
54
+ granted. No grant is fabricated and no token is minted here.
55
+ 6. **Start every leg before any byte moves.** A later leg refusing to start
56
+ closes the earlier ones `execution.failed` and returns `start-refused`. The
57
+ workspace is untouched by construction, not by cleanup.
58
+ 7. **Custody, then revalidation under it.** The lock is taken first and the plan
59
+ is revalidated afterwards, because a revalidation that precedes the lock
60
+ proves only what was true before another writer could act.
61
+ 8. **Stage, journal, apply.** New bytes and preimages are written and fsynced
62
+ into a staging directory on the same filesystem, then a journal naming the
63
+ before-state and the after-state of every endpoint is written and fsynced,
64
+ and only then does anything move.
65
+ 9. **Read the workspace back.** The outcome is what the readback proved, never
66
+ what the applying code believed: a `rename` that returned zero and a `rename`
67
+ whose effect a crash lost look identical from inside the process that made
68
+ the call.
69
+
70
+ ## The three outcomes
71
+
72
+ | readback | broker result | log |
73
+ |---|---|---|
74
+ | every endpoint is the after-state | `ok`, `state: "after"` | `execution.completed` on every leg |
75
+ | every endpoint is the before-state | `commit-not-applied` | `execution.failed` on every leg |
76
+ | anything else | `commit-unknown` | `execution.indeterminate`, reason `workspace-commit-unknown`, on every leg |
77
+
78
+ `workspace-commit-unknown` is new to SPEC.md §8's closed reason set and to
79
+ `schema/event.schema.json` (APRV-325.2). It is the local counterpart of
80
+ `act-threw`: POSIX offers no atomic multi-file rename, so a crash, a permission
81
+ error or a failed rollback can land between two of them. It is a separate member
82
+ rather than a reuse of the first because a reader deciding what to do next needs
83
+ to know whether the unknown effect was a remote call or a half-written tree.
84
+
85
+ A mixed commit retains both the transaction journal and the workspace lock, so
86
+ no further brokered change can run until a person has looked.
87
+ `approval codex recover` reads that journal and reports `before`, `after` or
88
+ `mixed`, and it changes nothing: rolling a mixed workspace forward would guess
89
+ which half the human approved, and rolling it back would delete the half that
90
+ committed. Resolution is `approval execution reconcile`, which is human-only.
91
+
92
+ ## What custody does and does not prove
93
+
94
+ The broker takes an `O_CREAT | O_EXCL` lock on a reserved name in the workspace.
95
+ That excludes every other broker that respects it and nothing else. It then asks
96
+ POSIX ownership and mode whether any principal other than its own effective user
97
+ can write the workspace root or the directories the transaction touches.
98
+
99
+ - `os-exclusive`: the lock is held and POSIX says no other principal can write.
100
+ - `advisory`: the lock is held and that second claim failed or could not be made.
101
+
102
+ Every report also carries `acl-unproven`, because ownership and mode say nothing
103
+ about ACLs — the same honesty `approval codex doctor --strict` already keeps.
104
+ `--require-exclusive-custody` turns the weak answer into a refusal instead of a
105
+ footnote, and an installation that needs the strong claim sets it.
106
+
107
+ Two paths inside the workspace are reserved and can never be a proposal
108
+ endpoint: `.approval-codex-txn/` and `.approval-codex-lock`. Staging must share
109
+ a filesystem with the workspace for `rename` to be atomic, so it lives inside
110
+ it; reserving the names is what stops a proposal from writing its own journal.
111
+
112
+ ## What this is not
113
+
114
+ This is a gate on **workspace writes**. It confines no shell, revokes no
115
+ credential and blocks no egress, and neither its presence nor a passing
116
+ `codex setup --check` makes a session enforced. A Codex session that can still
117
+ write the workspace by some other route is not bounded by this broker, and
118
+ proving that no such route exists is APRV-325.3's job, not this one's.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "approval-md",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Human approval for agent actions. CLI runtime for the approval.md convention (pre-release).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,6 +23,7 @@
23
23
  "LICENSE",
24
24
  "NOTICE",
25
25
  "docs/codex-enforced-session.md",
26
+ "docs/codex-workspace-broker.md",
26
27
  "templates/codex"
27
28
  ],
28
29
  "repository": "github:approval-md/approval.md",
@@ -38,6 +39,7 @@
38
39
  "typecheck": "tsc --noEmit",
39
40
  "test": "tsc -p tsconfig.json && node scripts/run-tests.mjs",
40
41
  "lint": "oxlint src tests",
42
+ "agent-hours": "node scripts/agent-hours.mjs",
41
43
  "ci:local": "node scripts/ci-local.mjs",
42
44
  "check:tier": "node scripts/classify-tier.mjs",
43
45
  "check:changed": "node scripts/classify-tier.mjs --working-tree --run",