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,424 @@
1
+ /**
2
+ * The unit of judgment: one commit (APRV-375).
3
+ *
4
+ * ## What a grant binds
5
+ *
6
+ * A `policy.edit` grant binds ONE edit: a before-state, an after-state, and a
7
+ * human who was shown them. The protected-path guard of
8
+ * `core/protected-path-guard.ts` decides whether a change is made of such
9
+ * material, and everything it answers is relative to the pair of blobs the
10
+ * caller hands it. So the caller's choice of pair is the whole question, and
11
+ * until this module the CI guard chose the pull request's COMBINED diff: base
12
+ * tree against head tree, every commit's work replayed as one change.
13
+ *
14
+ * That pairing has no grant. PR #427 is the worked example (APRV-357's
15
+ * reproduction, PR #452): three lane commits, each granted, plus two merges of
16
+ * `origin/main` made while four other lanes were editing `SPEC.md`. Replayed as
17
+ * one change it failed `uncovered-hunk` with fourteen uncovered lines, twenty
18
+ * nine records naming the path and twenty seven covering part of it, because a
19
+ * grant binds a before-state that the combined base no longer carries once an
20
+ * earlier commit in the range has moved those bytes. Replayed per commit all
21
+ * three pass, in well under a second each.
22
+ *
23
+ * `approval doctor`'s dark-session arm A reached the same conclusion from the
24
+ * other side (APRV-369): it used to replay a whole window as one change and
25
+ * failed a change CI had passed, at every session start. It now replays per
26
+ * commit. This module is where the two meet, so the agreement is by
27
+ * construction rather than by two code paths kept in step.
28
+ *
29
+ * ## The security argument
30
+ *
31
+ * Judging per commit asks a SMALLER question than judging the combined diff, so
32
+ * it owes an argument that nothing escapes. It is this:
33
+ *
34
+ * 1. Every byte that differs between the base tree and the head tree was
35
+ * written by some commit in `base..head`. Two-dot range semantics is what
36
+ * makes that true: a commit reachable from base is excluded, and a commit
37
+ * reachable from base changed nothing about the difference between the two
38
+ * trees that its own history did not already carry.
39
+ * 2. A non-merge commit's whole contribution is its diff against its parent,
40
+ * which is exactly what this module hands the guard.
41
+ * 3. A merge commit contributes only what it resolved: the bytes its result
42
+ * carries that no parent carried. That is git's DENSE combined diff
43
+ * (`--cc`), and a merge with a non-empty one on a guarded path is judged
44
+ * against its first parent here. A merge whose result on a path is some
45
+ * parent's bytes verbatim invented nothing and is listed as skipped.
46
+ *
47
+ * Together: every changed byte is inside some judged unit, and each unit is
48
+ * judged against the before-state a grant for it would have bound. The guard
49
+ * stops asking about the combined diff because the combined diff is a change
50
+ * nobody ever made and nobody could have approved.
51
+ *
52
+ * ## One thing is NOT per commit: whole-file evidence
53
+ *
54
+ * A grant binds a hunk, so it is evidence about one commit. A sign-off
55
+ * (APRV-338), an organ attestation (APRV-272) and the policy attestation say a
56
+ * human read the file AS IT NOW STANDS, so they are evidence about the bytes
57
+ * the range INSTALLS: every commit is offered the digests at the RANGE HEAD.
58
+ * {@link digestsAt} carries the reasoning, and it matters because matching them
59
+ * per commit would have repealed the escape hatch for every edit before the
60
+ * last one.
61
+ *
62
+ * ## What this does NOT do
63
+ *
64
+ * It does not re-judge work the branch absorbed by merging `origin/main`. Those
65
+ * commits are main's, they passed their own pull requests, and two-dot
66
+ * `base..head` excludes them by construction: CI computes base as `git
67
+ * merge-base origin/main HEAD`, so a branch that merged main has every absorbed
68
+ * commit reachable from base. Nothing is skipped by a rule about authorship or
69
+ * about who pushed what; the range is simply the commits this pull request is
70
+ * proposing to add.
71
+ *
72
+ * It also does not relax the exact-replay byte budget of
73
+ * `core/protected-path-guard.ts`. Per commit that budget is not reached in any
74
+ * case observed, which is why APRV-357 closes as superseded rather than fixed.
75
+ */
76
+ import { createHash } from "node:crypto";
77
+ import { evaluateProtectedPaths, isGuardedPath } from "./protected-path-guard.js";
78
+ /** SHA-256 of a UTF-8 string, hex. */
79
+ function sha256Hex(text) {
80
+ return createHash("sha256").update(text, "utf8").digest("hex");
81
+ }
82
+ /** Is this commit a merge? */
83
+ export function isMerge(commit) {
84
+ return commit.parents.length > 1;
85
+ }
86
+ /**
87
+ * The revision a commit is judged AGAINST: its first parent.
88
+ *
89
+ * First parent and not the merge base of the parents, for a merge, because the
90
+ * first parent is the state the branch was in before the merge and the merge's
91
+ * own contribution is measured from there. `null` for a root commit, which has
92
+ * no before-state at all and whose every path is an add.
93
+ */
94
+ export function baseRevOf(commit) {
95
+ return commit.parents.length === 0 ? null : `${commit.sha}^1`;
96
+ }
97
+ /**
98
+ * Record and field separators inside `git log --pretty`.
99
+ *
100
+ * ASCII 30 and 31, for the reason `core/dark-session.ts` states where it uses
101
+ * the same pair: git quotes any path containing a control character, so a
102
+ * filename engineered to carry one arrives escaped and cannot forge a commit
103
+ * boundary in output the guard parses.
104
+ */
105
+ const RS = "";
106
+ const FS = "";
107
+ function datesOf(author, committer) {
108
+ const a = author === undefined || author.length === 0 ? null : author;
109
+ const c = committer === undefined || committer.length === 0 ? null : committer;
110
+ return { author: a, committer: c ?? a };
111
+ }
112
+ /**
113
+ * The commits of `base..head`, oldest first, each with the paths it is judged
114
+ * for.
115
+ *
116
+ * `judged` narrows the per-path work a MERGE costs: git's dense combined diff
117
+ * has to be asked for one path at a time (see below), and asking it about every
118
+ * file a large merge touched would be a git invocation per file for no verdict.
119
+ * A non-merge commit's path list is not narrowed at all, so the guard still
120
+ * reports exempt paths and still sees everything it might protect.
121
+ *
122
+ * The two-step merge question is not an optimization, it is correctness.
123
+ * `git log -c --name-only` lists the files "modified from all parents", which
124
+ * OVER-reports: at PR #427's merge `e66ba9e` it lists `SPEC.md`, whose result
125
+ * differs from both parents as a blob while every hunk of it came verbatim from
126
+ * one side. The dense (`--cc`) rendering is the one that drops hunks a parent
127
+ * already carried, and a path whose dense rendering is empty invented nothing.
128
+ * So each candidate is re-asked with a per-path `--cc` patch, and passing the
129
+ * path as a pathspec rather than parsing it out of a `diff --cc` header keeps
130
+ * quoted and unusual filenames out of the parser entirely.
131
+ */
132
+ export function listCommits(read, base, head, judged) {
133
+ const log = read([
134
+ "log",
135
+ "--reverse",
136
+ `--pretty=format:${RS}%H${FS}%P${FS}%aI${FS}%cI`,
137
+ "--name-only",
138
+ `${base}..${head}`,
139
+ ]);
140
+ if (log === null) {
141
+ return { ok: false, message: `\`git log ${base}..${head}\` failed` };
142
+ }
143
+ const commits = [];
144
+ for (const chunk of log.split(RS)) {
145
+ const lines = chunk.split("\n").filter((line) => line.length > 0);
146
+ const header = lines.shift();
147
+ if (header === undefined)
148
+ continue;
149
+ const [sha, parents, authored, committed] = header.split(FS);
150
+ if (sha === undefined || sha.length === 0)
151
+ continue;
152
+ const parentList = (parents ?? "")
153
+ .split(" ")
154
+ .map((part) => part.trim())
155
+ .filter((part) => part.length > 0);
156
+ const commit = {
157
+ sha,
158
+ parents: parentList,
159
+ ts: datesOf(authored, committed),
160
+ // `--name-only` reports nothing for a merge; the dense pass below fills it.
161
+ changedPaths: parentList.length > 1 ? denseCombinedPaths(read, sha, judged) : lines,
162
+ };
163
+ commits.push(commit);
164
+ }
165
+ return { ok: true, commits };
166
+ }
167
+ /**
168
+ * The guarded paths a merge commit's result carries that no parent carried:
169
+ * the merge's own work, which is what an evil merge is made of.
170
+ *
171
+ * EXPORTED because arm A of `core/dark-session.ts` asks the same question of a
172
+ * merge it finds on main (APRV-374), and two implementations of "what did this
173
+ * merge invent" would be two things to keep in step — the disagreement APRV-369
174
+ * was filed about. The observer there lists a window rather than a range, so it
175
+ * cannot reuse {@link listCommits}; it reuses this.
176
+ *
177
+ * The two-step shape is the correctness (see {@link listCommits}): `-c
178
+ * --name-only` gives candidates and over-reports, and the per-path `--cc` patch
179
+ * is the discriminator, empty for every merge whose every hunk came from a
180
+ * parent verbatim.
181
+ */
182
+ export function denseCombinedPaths(read, sha, judged) {
183
+ const listed = read(["log", "-1", "-c", "--name-only", "--pretty=format:", sha]);
184
+ if (listed === null)
185
+ return [];
186
+ const dense = [];
187
+ for (const line of listed.split("\n")) {
188
+ const path = line.trim();
189
+ if (path.length === 0 || !judged(path))
190
+ continue;
191
+ const patch = read(["log", "-1", "--cc", "--pretty=format:", sha, "--", path]);
192
+ if (patch !== null && patch.trim().length > 0)
193
+ dense.push(path);
194
+ }
195
+ return dense;
196
+ }
197
+ export function commitGuardInputParts(read, commit) {
198
+ const parents = commit.parents;
199
+ const baseRev = parents !== undefined && parents.length === 0 ? null : `${commit.sha}^1`;
200
+ const blobCache = new Map();
201
+ const digestCache = new Map();
202
+ const inTree = (rev, path) => {
203
+ const listed = read(["ls-tree", "--name-only", rev, "--", path]);
204
+ return listed !== null && listed.trim().length > 0;
205
+ };
206
+ const show = (rev, path) => read(["show", `${rev}:${path}`]);
207
+ return {
208
+ blobsFor: (path) => {
209
+ const cached = blobCache.get(path);
210
+ if (cached !== undefined)
211
+ return cached;
212
+ const baseHas = baseRev !== null && inTree(baseRev, path);
213
+ const headHas = inTree(commit.sha, path);
214
+ const baseText = baseHas && baseRev !== null ? show(baseRev, path) : null;
215
+ const headText = headHas ? show(commit.sha, path) : null;
216
+ // A blob git cannot show, or one with a NUL byte in it, answers null and
217
+ // the guard fails the path `change-unreadable` rather than falling back
218
+ // to the path-level rule. The fallback is the hole (APRV-202).
219
+ const unreadable = (baseHas && baseText === null) ||
220
+ (headHas && headText === null) ||
221
+ (baseText !== null && baseText.includes("")) ||
222
+ (headText !== null && headText.includes(""));
223
+ const value = unreadable ? null : { base: baseText, head: headText };
224
+ blobCache.set(path, value);
225
+ return value;
226
+ },
227
+ sha256At: (path) => {
228
+ const cached = digestCache.get(path);
229
+ if (cached !== undefined)
230
+ return cached;
231
+ const blob = show(commit.sha, path);
232
+ const value = blob === null ? null : sha256Hex(blob);
233
+ digestCache.set(path, value);
234
+ return value;
235
+ },
236
+ changeTsFor: () => commit.ts,
237
+ window: {
238
+ base: baseRev === null ? "(root)" : `${commit.sha.slice(0, 12)}^1`,
239
+ head: commit.sha.slice(0, 12),
240
+ },
241
+ };
242
+ }
243
+ /**
244
+ * SHA-256 of any path's bytes AT ONE REVISION, cached.
245
+ *
246
+ * ## Why whole-file evidence is matched at the range head and not per commit
247
+ *
248
+ * The two kinds of evidence answer different questions, so they are anchored to
249
+ * different things:
250
+ *
251
+ * - A GRANT binds a hunk — a before-state, an after-state, a human who was
252
+ * shown them — so it is evidence about ONE COMMIT and is matched per commit,
253
+ * which is what APRV-375 is about.
254
+ * - A SIGN-OFF (`gate.path.signed_off`, APRV-338), an ORGAN attestation
255
+ * (`gate.organ.attested`, APRV-272) and the POLICY attestation
256
+ * (`policy.updated`) are whole-file records: a human read the file AS IT NOW
257
+ * STANDS and ratified those exact bytes. What they are about is the bytes the
258
+ * pull request INSTALLS, which is the blob at the range head.
259
+ *
260
+ * Matching them per commit would have quietly repealed the escape hatch. A
261
+ * two-commit branch whose first commit has no grant could no longer be rescued
262
+ * by a sign-off at head, because the intermediate blob is not the ratified one;
263
+ * a policy amendment split over two commits would fail on the first. So every
264
+ * commit in a range is offered the digests at the range head, whatever its own
265
+ * blob hashes to. The ordering APRV-338 asked for is untouched: the evaluator
266
+ * reaches the sign-off only after every grant search has failed, so hunk
267
+ * evidence still leads the reasons wherever it exists.
268
+ */
269
+ export function digestsAt(read, rev) {
270
+ const cache = new Map();
271
+ return (path) => {
272
+ const cached = cache.get(path);
273
+ if (cached !== undefined)
274
+ return cached;
275
+ const blob = read(["show", `${rev}:${path}`]);
276
+ const value = blob === null ? null : sha256Hex(blob);
277
+ cache.set(path, value);
278
+ return value;
279
+ };
280
+ }
281
+ /**
282
+ * Judge every commit of `base..head` on its own records, and report the
283
+ * conjunction.
284
+ *
285
+ * `evaluateProtectedPaths` is unchanged and is what decides each commit: this
286
+ * function chooses the unit and nothing else. A commit that changed no guarded
287
+ * path costs one evaluator call that finds nothing, and a clean merge costs no
288
+ * evaluator call at all.
289
+ */
290
+ export function judgeCommits(options) {
291
+ const { read, base, head, shared } = options;
292
+ const empty = {
293
+ ok: false,
294
+ commits: [],
295
+ findings: [],
296
+ exempt: [],
297
+ window: shared.window,
298
+ };
299
+ const listed = listCommits(read, base, head, (path) => isJudgedPath(path, shared));
300
+ if (!listed.ok)
301
+ return { ...empty, unavailable: listed.message };
302
+ // Whole-file evidence is about the bytes this pull request INSTALLS, so it is
303
+ // matched at the RANGE head for every commit in the range. See {@link digestsAt}.
304
+ const atRangeHead = digestsAt(read, head);
305
+ const policySha256AtHead = atRangeHead(shared.policyPath);
306
+ const commits = [];
307
+ const findings = [];
308
+ const exempt = new Set();
309
+ for (const commit of listed.commits) {
310
+ const merge = isMerge(commit);
311
+ const baseRev = baseRevOf(commit);
312
+ if (commit.changedPaths.length === 0) {
313
+ commits.push({
314
+ sha: commit.sha,
315
+ base: baseRev,
316
+ merge,
317
+ judged: false,
318
+ skipped: merge ? "clean-merge" : "no-guarded-path",
319
+ ok: true,
320
+ findings: [],
321
+ exempt: [],
322
+ covered: [],
323
+ });
324
+ continue;
325
+ }
326
+ const parts = commitGuardInputParts(read, commit);
327
+ const report = evaluateProtectedPaths({
328
+ ...shared,
329
+ policySha256AtHead,
330
+ changedPaths: [...commit.changedPaths].sort(),
331
+ blobsFor: parts.blobsFor,
332
+ organSha256AtHead: atRangeHead,
333
+ pathSha256AtHead: atRangeHead,
334
+ changeTsFor: parts.changeTsFor,
335
+ window: { ...shared.window, base: parts.window.base, head: parts.window.head },
336
+ });
337
+ for (const path of report.exempt)
338
+ exempt.add(path);
339
+ for (const finding of report.findings)
340
+ findings.push({ ...finding, commit: commit.sha });
341
+ const covered = [];
342
+ for (const finding of report.findings) {
343
+ if (!finding.ok)
344
+ continue;
345
+ for (const seq of finding.coveredBy ?? (finding.seq === undefined ? [] : [finding.seq])) {
346
+ if (!covered.includes(seq))
347
+ covered.push(seq);
348
+ }
349
+ }
350
+ const nothingToJudge = report.findings.length === 0 && report.exempt.length === 0;
351
+ commits.push({
352
+ sha: commit.sha,
353
+ base: baseRev,
354
+ merge,
355
+ judged: !nothingToJudge,
356
+ skipped: nothingToJudge ? (merge ? "clean-merge" : "no-guarded-path") : null,
357
+ ok: report.ok,
358
+ findings: report.findings,
359
+ exempt: report.exempt,
360
+ covered,
361
+ });
362
+ }
363
+ return {
364
+ ok: commits.every((commit) => commit.ok),
365
+ commits,
366
+ findings,
367
+ exempt: [...exempt],
368
+ window: shared.window,
369
+ unavailable: null,
370
+ };
371
+ }
372
+ /**
373
+ * Would the guard have an opinion about this path?
374
+ *
375
+ * Only used to keep the merge pass cheap, so it is deliberately the PERMISSIVE
376
+ * side of the question: the policy's widening entries plus the built-in set,
377
+ * which is what `isGuardedPath` asks. A path this answers `true` for still has
378
+ * to earn its verdict from the evaluator.
379
+ */
380
+ function isJudgedPath(path, shared) {
381
+ return isGuardedPath(path, shared.policyProtectedPaths);
382
+ }
383
+ /** The report as the lines CI prints. Pure. */
384
+ export function renderCommitGuardReport(report) {
385
+ const lines = [];
386
+ const judged = report.commits.filter((commit) => commit.judged);
387
+ lines.push(`protected-path guard: ${report.window.base}..${report.window.head}, judged one commit at a time (${String(report.commits.length)} commit(s) in the range, ${String(judged.length)} with a protected change)`);
388
+ if (report.unavailable !== null) {
389
+ lines.push(` the guard could not look: ${report.unavailable}`);
390
+ return `${lines.join("\n")}\n`;
391
+ }
392
+ if (report.exempt.length > 0) {
393
+ lines.push(` exempt (the daemon's own append surface, evidence rather than a protected write): ${report.exempt.join(", ")}`);
394
+ }
395
+ if (report.commits.length === 0) {
396
+ lines.push(" no commits in the range");
397
+ return `${lines.join("\n")}\n`;
398
+ }
399
+ for (const commit of report.commits) {
400
+ const sha = commit.sha.slice(0, 12);
401
+ const against = commit.base === null ? "a root commit" : `base ${sha}^1`;
402
+ if (!commit.judged) {
403
+ const why = commit.skipped === "clean-merge"
404
+ ? "merge, and its result on every guarded path is one parent's bytes verbatim"
405
+ : "no protected path changed";
406
+ lines.push(` SKIP ${sha} (${against}): ${why}`);
407
+ continue;
408
+ }
409
+ const covered = commit.covered.length === 0
410
+ ? ""
411
+ : `, covered by seq ${commit.covered.slice(0, 8).join(", ")}${commit.covered.length > 8 ? ", …" : ""}`;
412
+ lines.push(` ${commit.ok ? "PASS" : "FAIL"} ${sha} (${against})${commit.merge ? ", merge resolution" : ""}${covered}`);
413
+ for (const finding of commit.findings) {
414
+ lines.push(finding.ok
415
+ ? ` PASS ${finding.path} [${finding.evidence}]`
416
+ : ` FAIL ${finding.path} [${finding.code}]`);
417
+ lines.push(` ${finding.detail}`);
418
+ }
419
+ }
420
+ if (judged.length === 0)
421
+ lines.push(" no protected paths changed");
422
+ return `${lines.join("\n")}\n`;
423
+ }
424
+ //# sourceMappingURL=commit-guard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commit-guard.js","sourceRoot":"","sources":["../../../src/core/commit-guard.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Is THIS process the daemon? (APRV-382.)
3
+ *
4
+ * One fact, held in one place, so the gated advance path can ask it without
5
+ * being told it. `log.advance.daemon` is autonomous in this repository's policy
6
+ * and `log.advance` is not, which means something has to decide which of the
7
+ * two a cycle asks under — and that decision must not be an argument a caller
8
+ * passes, because a field a caller sets is a self-reported field, and SPEC.md
9
+ * §11.1 invariant 4 says a self-reported field never reduces scrutiny.
10
+ *
11
+ * So the mark is set by the daemon runtime itself, at construction, and read
12
+ * from module state that no exported function will set on another process's
13
+ * behalf: {@link markDaemonProcess} takes no arguments and records
14
+ * `process.pid`, and {@link isDaemonProcess} answers true only while that
15
+ * recorded pid is still this process's own. A child inherits nothing — module
16
+ * state does not cross a `spawn` — so the advance child, which holds no
17
+ * authority by design, answers false here as it should.
18
+ *
19
+ * What this is NOT: a security boundary against code already running in the
20
+ * daemon's process. Anything executing there can call {@link markDaemonProcess}
21
+ * the way the daemon does. The boundary that keeps an agent out of this process
22
+ * is elsewhere and unchanged: `approval daemon run` and `approval up` classify
23
+ * `gate.self`, which this repository's policy leaves at the fail-closed manual
24
+ * default, so an agent cannot start a daemon unattended in the first place.
25
+ * What this module removes is the smaller and likelier failure — a future
26
+ * caller wiring the autonomous route through a struct field, in a session, by
27
+ * accident.
28
+ */
29
+ /**
30
+ * Declare this process the daemon runtime. Called by the daemon, and only by it.
31
+ *
32
+ * Idempotent, and argument-free on purpose: there is no spelling of this call
33
+ * that marks a process other than the caller's own.
34
+ */
35
+ export declare function markDaemonProcess(): void;
36
+ /**
37
+ * Forget the mark. For tests, and for a runtime that stops being a daemon.
38
+ *
39
+ * Present so a suite can exercise both actors in one file without a second
40
+ * process. Clearing is always the STRICTER direction — an unmarked process
41
+ * takes the supervised route — so nothing widens by calling it.
42
+ */
43
+ export declare function clearDaemonProcess(): void;
44
+ /** Is this process the daemon runtime? */
45
+ export declare function isDaemonProcess(): boolean;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Is THIS process the daemon? (APRV-382.)
3
+ *
4
+ * One fact, held in one place, so the gated advance path can ask it without
5
+ * being told it. `log.advance.daemon` is autonomous in this repository's policy
6
+ * and `log.advance` is not, which means something has to decide which of the
7
+ * two a cycle asks under — and that decision must not be an argument a caller
8
+ * passes, because a field a caller sets is a self-reported field, and SPEC.md
9
+ * §11.1 invariant 4 says a self-reported field never reduces scrutiny.
10
+ *
11
+ * So the mark is set by the daemon runtime itself, at construction, and read
12
+ * from module state that no exported function will set on another process's
13
+ * behalf: {@link markDaemonProcess} takes no arguments and records
14
+ * `process.pid`, and {@link isDaemonProcess} answers true only while that
15
+ * recorded pid is still this process's own. A child inherits nothing — module
16
+ * state does not cross a `spawn` — so the advance child, which holds no
17
+ * authority by design, answers false here as it should.
18
+ *
19
+ * What this is NOT: a security boundary against code already running in the
20
+ * daemon's process. Anything executing there can call {@link markDaemonProcess}
21
+ * the way the daemon does. The boundary that keeps an agent out of this process
22
+ * is elsewhere and unchanged: `approval daemon run` and `approval up` classify
23
+ * `gate.self`, which this repository's policy leaves at the fail-closed manual
24
+ * default, so an agent cannot start a daemon unattended in the first place.
25
+ * What this module removes is the smaller and likelier failure — a future
26
+ * caller wiring the autonomous route through a struct field, in a session, by
27
+ * accident.
28
+ */
29
+ /** The pid of the process that declared itself the daemon, or `null`. */
30
+ let daemonPid = null;
31
+ /**
32
+ * Declare this process the daemon runtime. Called by the daemon, and only by it.
33
+ *
34
+ * Idempotent, and argument-free on purpose: there is no spelling of this call
35
+ * that marks a process other than the caller's own.
36
+ */
37
+ export function markDaemonProcess() {
38
+ daemonPid = process.pid;
39
+ }
40
+ /**
41
+ * Forget the mark. For tests, and for a runtime that stops being a daemon.
42
+ *
43
+ * Present so a suite can exercise both actors in one file without a second
44
+ * process. Clearing is always the STRICTER direction — an unmarked process
45
+ * takes the supervised route — so nothing widens by calling it.
46
+ */
47
+ export function clearDaemonProcess() {
48
+ daemonPid = null;
49
+ }
50
+ /** Is this process the daemon runtime? */
51
+ export function isDaemonProcess() {
52
+ return daemonPid !== null && daemonPid === process.pid;
53
+ }
54
+ //# sourceMappingURL=daemon-actor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon-actor.js","sourceRoot":"","sources":["../../../src/core/daemon-actor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,yEAAyE;AACzE,IAAI,SAAS,GAAkB,IAAI,CAAC;AAEpC;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB;IAC/B,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC;AAC1B,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB;IAChC,SAAS,GAAG,IAAI,CAAC;AACnB,CAAC;AAED,0CAA0C;AAC1C,MAAM,UAAU,eAAe;IAC7B,OAAO,SAAS,KAAK,IAAI,IAAI,SAAS,KAAK,OAAO,CAAC,GAAG,CAAC;AACzD,CAAC"}