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,172 @@
1
+ /**
2
+ * Read scope: which directories an agent may read from (APRV-347).
3
+ *
4
+ * ## The hole this closes
5
+ *
6
+ * Writes and deletes have been path-scoped for a while. `files.delete.scratch`
7
+ * versus `files.delete.out_of_scope` is decided by comparing a resolved target
8
+ * against roots the caller supplied (`ClassifierContext.scratchRoots`), and the
9
+ * file tools carry their target into the payload a grant binds. Reads had none
10
+ * of that: every shell reader classified `read.shell` with no path bound, and
11
+ * `Read`, `Glob` and `Grep` were answered `allow` before classification ever
12
+ * ran. So a policy could say a great deal about what an agent may WRITE and
13
+ * nothing at all about what it may SEE, and an agent working in one directory
14
+ * could read every sibling of it.
15
+ *
16
+ * This module is the pure half of the read-side mirror. It holds the class
17
+ * name, the roots arithmetic, and the one genuinely fiddly question — which
18
+ * words of a read command are paths — and it touches no disk, reads no
19
+ * environment and resolves nothing. The impure half (relative paths resolved
20
+ * against a working directory, symlinks followed, the escape that only the
21
+ * filesystem can see) lives in `src/cli/hook.ts`, exactly where the delete
22
+ * rule's second pass lives, and it can only ever TIGHTEN this file's answer.
23
+ *
24
+ * ## Fail closed, in three places
25
+ *
26
+ * SPEC.md §11.1: ambiguity resolves to the stricter path. Here that is
27
+ *
28
+ * 1. a target this file cannot read as a path (a `$VAR`, a glob, a `~`) is out
29
+ * of scope, because what it expands to is not in the text;
30
+ * 2. a read command naming NO target reads the working directory, so it is
31
+ * checked against the working directory rather than waved through;
32
+ * 3. an empty root list means nothing is in scope — but a caller that passes no
33
+ * roots at all gets today's answer instead (see {@link ClassifierContext}),
34
+ * because a caller that forgot the field must not have every read it makes
35
+ * turned into a decision.
36
+ *
37
+ * ## What a root is
38
+ *
39
+ * The gate root (the directory holding the policy file the runtime resolved),
40
+ * the session scratchpad, and the system temp root. A policy may WIDEN that
41
+ * with `read_scope.roots`; it may not narrow it below the gate root, because a
42
+ * runtime that cannot read its own policy, log and workspace cannot run at all.
43
+ */
44
+ /**
45
+ * The class a read outside every root takes.
46
+ *
47
+ * A sibling of `read.shell` rather than a replacement for it: a read INSIDE the
48
+ * roots is the same ordinary, autonomous act it has always been, and a policy
49
+ * that says nothing about this class gets `defaults.autonomy` for it, which is
50
+ * the fail-closed direction for a name nobody has declared.
51
+ */
52
+ export declare const READ_OUT_OF_SCOPE_CLASS = "read.file.out_of_scope";
53
+ /**
54
+ * The `read_scope` block of a policy (SPEC.md §5, amended APRV-347).
55
+ *
56
+ * Additive and optional, with the same discipline `protected_paths` has: the
57
+ * built-in roots stand whatever this says, so a policy can widen the scope and
58
+ * never shrink it. A relative entry is resolved against the gate root, so a
59
+ * policy stays portable between a checkout and a clone of it.
60
+ */
61
+ export interface ReadScope {
62
+ roots?: string[];
63
+ }
64
+ /**
65
+ * Is `candidate` AT or under `root`, by path segment?
66
+ *
67
+ * At-or-under rather than the delete rule's strictly-under: `ls <gate root>` is
68
+ * a read of the workspace an agent is working in, and a rule that made the root
69
+ * itself out of scope would classify the most ordinary command in the session.
70
+ *
71
+ * Segment matching, never string prefixes: `/dev/muse-other` must not match a
72
+ * root of `/dev/muse`, and `startsWith` says it does.
73
+ */
74
+ export declare function isAtOrUnderReadRoot(candidate: string, root: string): boolean;
75
+ /** Is this path inside ANY of these roots? */
76
+ export declare function isInReadScope(candidate: string, roots: readonly string[]): boolean;
77
+ /**
78
+ * A value whose expansion the classifier cannot see, and therefore may not
79
+ * vouch for. The same test the delete rule applies, and for the same reason:
80
+ * `cat $SOMEWHERE` reads whatever that variable holds.
81
+ */
82
+ export declare function isUnreadableTarget(word: string): boolean;
83
+ /**
84
+ * The effective read roots: the built-ins, plus whatever the policy added.
85
+ *
86
+ * Pure, and every input is the caller's. `gateRoot` is the directory holding
87
+ * the policy file the runtime resolved; `systemRoots` are the scratchpad and
88
+ * temp roots the caller already resolved (`resolveScratchRoots` in the hook);
89
+ * `declared` is `read_scope.roots` verbatim.
90
+ *
91
+ * A declared entry that is relative is joined onto the gate root. A declared
92
+ * entry the caller cannot vouch for — empty, or one this file can see is not a
93
+ * path at all — is DROPPED rather than accepted, because a root is an
94
+ * authorization and a malformed one must not become `/`.
95
+ *
96
+ * The result is de-duplicated and otherwise in the order given, so the first
97
+ * root a path matches is the most specific one a reader would expect.
98
+ */
99
+ export declare function effectiveReadRoots(options: {
100
+ gateRoot: string;
101
+ declared?: readonly string[] | undefined;
102
+ systemRoots?: readonly string[] | undefined;
103
+ }): string[];
104
+ /**
105
+ * How a reader's positionals map to paths.
106
+ *
107
+ * - `all`: every positional is a file or directory (`cat a b`, `ls src`,
108
+ * `diff a b`, `cut -d: -f1 /etc/passwd` — `cut`'s delimiter and field list
109
+ * are flags, so none of its positionals is a pattern).
110
+ * - `after-pattern`: the FIRST positional is a pattern or a script and the rest
111
+ * are paths (`grep needle src`, `sed -n 1,5p file`, `jq .x file.json`) —
112
+ * UNLESS the pattern arrived through a flag (`-e`, `-f`), in which case every
113
+ * positional is a path and the shape collapses to `all`.
114
+ * - `walk`: `find`'s shape — positionals up to the first primary are paths.
115
+ *
116
+ * Binaries absent from this table are absent on purpose, and each omission is a
117
+ * decision not to widen anything:
118
+ *
119
+ * - `echo`, `printf`, `tr`, `test`, `type`, `which`, `pwd`, `true`, `false`,
120
+ * `cd`: their positionals are not files, or name a file without reading its
121
+ * contents. A rule that treated `echo /etc/passwd` as a read of that file
122
+ * would route text through a human.
123
+ * - `basename`, `dirname`, `readlink`, `realpath`: they manipulate or resolve a
124
+ * path and never open it. What leaks is the existence of a name, which is not
125
+ * what this class is about.
126
+ * - `less` and `more` are NOT added to the classifier's reader list by this
127
+ * task. Adding them would take them from `unclassified` (a deny) to
128
+ * `read.shell` (this repository's policy: autonomous), which is a widening,
129
+ * and a task that exists to narrow reads has no business doing that in
130
+ * passing. They are named in the follow-up in `docs/sandboxed-exec.md`.
131
+ */
132
+ export type ReadTargetShape = "all" | "after-pattern" | "walk";
133
+ /** Which readers take paths, and where. Keyed by the binary's basename. */
134
+ export declare const READ_TARGET_SHAPES: Readonly<Record<string, ReadTargetShape>>;
135
+ /**
136
+ * The paths a read command will open, or `null` when this binary is not one
137
+ * whose reads this module scopes.
138
+ *
139
+ * An empty array is a real answer and is NOT the same as `null`: it means this
140
+ * reader opens the working directory (`ls`, `find`, `grep needle` with no file
141
+ * operand), and the caller checks the working directory in its place. `null`
142
+ * means "not a scoped reader", and the caller leaves the segment alone.
143
+ *
144
+ * Treating a non-path positional as a path costs nothing: a bare word resolves
145
+ * against the working directory, which is inside a root in every session this
146
+ * module is meant for. Treating a path as a non-path costs the whole property.
147
+ */
148
+ export declare function readTargetsOf(bin: string, positionals: readonly string[], args?: readonly string[]): string[] | null;
149
+ /**
150
+ * The verdict the PURE half can reach for one target.
151
+ *
152
+ * `out-of-scope` and `in-scope` are final. `needs-disk` is the honest answer
153
+ * for a relative path: its meaning depends on a working directory this file
154
+ * does not have, and the caller with the disk decides it. A caller that cannot
155
+ * do the second pass must treat `needs-disk` as out of scope — which is what
156
+ * `hook classify` and `hook <harness>` both do, through the same function.
157
+ */
158
+ export type ReadTargetVerdict = "in-scope" | "out-of-scope" | "needs-disk";
159
+ /**
160
+ * Read one target against the roots, as far as text alone can settle it.
161
+ *
162
+ * Absolute and inside a root: in scope. Absolute and outside every root: out of
163
+ * scope, decided here, no disk needed. Unreadable (a variable, a glob, a `~`):
164
+ * out of scope, because what it names is not in the text. Anything relative, or
165
+ * carrying a `..`, is `needs-disk`.
166
+ */
167
+ export declare function readTargetVerdict(target: string, roots: readonly string[]): ReadTargetVerdict;
168
+ /**
169
+ * The roots, rendered for a human: `approval policy check`'s line and the
170
+ * hook's verdict note say the same sentence.
171
+ */
172
+ export declare function renderReadRoots(roots: readonly string[]): string;
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Read scope: which directories an agent may read from (APRV-347).
3
+ *
4
+ * ## The hole this closes
5
+ *
6
+ * Writes and deletes have been path-scoped for a while. `files.delete.scratch`
7
+ * versus `files.delete.out_of_scope` is decided by comparing a resolved target
8
+ * against roots the caller supplied (`ClassifierContext.scratchRoots`), and the
9
+ * file tools carry their target into the payload a grant binds. Reads had none
10
+ * of that: every shell reader classified `read.shell` with no path bound, and
11
+ * `Read`, `Glob` and `Grep` were answered `allow` before classification ever
12
+ * ran. So a policy could say a great deal about what an agent may WRITE and
13
+ * nothing at all about what it may SEE, and an agent working in one directory
14
+ * could read every sibling of it.
15
+ *
16
+ * This module is the pure half of the read-side mirror. It holds the class
17
+ * name, the roots arithmetic, and the one genuinely fiddly question — which
18
+ * words of a read command are paths — and it touches no disk, reads no
19
+ * environment and resolves nothing. The impure half (relative paths resolved
20
+ * against a working directory, symlinks followed, the escape that only the
21
+ * filesystem can see) lives in `src/cli/hook.ts`, exactly where the delete
22
+ * rule's second pass lives, and it can only ever TIGHTEN this file's answer.
23
+ *
24
+ * ## Fail closed, in three places
25
+ *
26
+ * SPEC.md §11.1: ambiguity resolves to the stricter path. Here that is
27
+ *
28
+ * 1. a target this file cannot read as a path (a `$VAR`, a glob, a `~`) is out
29
+ * of scope, because what it expands to is not in the text;
30
+ * 2. a read command naming NO target reads the working directory, so it is
31
+ * checked against the working directory rather than waved through;
32
+ * 3. an empty root list means nothing is in scope — but a caller that passes no
33
+ * roots at all gets today's answer instead (see {@link ClassifierContext}),
34
+ * because a caller that forgot the field must not have every read it makes
35
+ * turned into a decision.
36
+ *
37
+ * ## What a root is
38
+ *
39
+ * The gate root (the directory holding the policy file the runtime resolved),
40
+ * the session scratchpad, and the system temp root. A policy may WIDEN that
41
+ * with `read_scope.roots`; it may not narrow it below the gate root, because a
42
+ * runtime that cannot read its own policy, log and workspace cannot run at all.
43
+ */
44
+ /**
45
+ * The class a read outside every root takes.
46
+ *
47
+ * A sibling of `read.shell` rather than a replacement for it: a read INSIDE the
48
+ * roots is the same ordinary, autonomous act it has always been, and a policy
49
+ * that says nothing about this class gets `defaults.autonomy` for it, which is
50
+ * the fail-closed direction for a name nobody has declared.
51
+ */
52
+ export const READ_OUT_OF_SCOPE_CLASS = "read.file.out_of_scope";
53
+ /** Non-empty path segments, `.` dropped. Identical to the classifier's own. */
54
+ function segmentsOf(candidate) {
55
+ return candidate
56
+ .split(/[/\\]+/u)
57
+ .filter((segment) => segment.length > 0 && segment !== ".");
58
+ }
59
+ /**
60
+ * Is `candidate` AT or under `root`, by path segment?
61
+ *
62
+ * At-or-under rather than the delete rule's strictly-under: `ls <gate root>` is
63
+ * a read of the workspace an agent is working in, and a rule that made the root
64
+ * itself out of scope would classify the most ordinary command in the session.
65
+ *
66
+ * Segment matching, never string prefixes: `/dev/muse-other` must not match a
67
+ * root of `/dev/muse`, and `startsWith` says it does.
68
+ */
69
+ export function isAtOrUnderReadRoot(candidate, root) {
70
+ const want = segmentsOf(root);
71
+ const have = segmentsOf(candidate);
72
+ if (want.length === 0)
73
+ return false;
74
+ if (have.length < want.length)
75
+ return false;
76
+ return want.every((segment, index) => segment === have[index]);
77
+ }
78
+ /** Is this path inside ANY of these roots? */
79
+ export function isInReadScope(candidate, roots) {
80
+ return roots.some((root) => isAtOrUnderReadRoot(candidate, root));
81
+ }
82
+ /**
83
+ * A value whose expansion the classifier cannot see, and therefore may not
84
+ * vouch for. The same test the delete rule applies, and for the same reason:
85
+ * `cat $SOMEWHERE` reads whatever that variable holds.
86
+ */
87
+ export function isUnreadableTarget(word) {
88
+ return (word.includes("$") ||
89
+ word.includes("*") ||
90
+ word.includes("?") ||
91
+ word.includes("[") ||
92
+ word.startsWith("~"));
93
+ }
94
+ /**
95
+ * The effective read roots: the built-ins, plus whatever the policy added.
96
+ *
97
+ * Pure, and every input is the caller's. `gateRoot` is the directory holding
98
+ * the policy file the runtime resolved; `systemRoots` are the scratchpad and
99
+ * temp roots the caller already resolved (`resolveScratchRoots` in the hook);
100
+ * `declared` is `read_scope.roots` verbatim.
101
+ *
102
+ * A declared entry that is relative is joined onto the gate root. A declared
103
+ * entry the caller cannot vouch for — empty, or one this file can see is not a
104
+ * path at all — is DROPPED rather than accepted, because a root is an
105
+ * authorization and a malformed one must not become `/`.
106
+ *
107
+ * The result is de-duplicated and otherwise in the order given, so the first
108
+ * root a path matches is the most specific one a reader would expect.
109
+ */
110
+ export function effectiveReadRoots(options) {
111
+ const roots = [];
112
+ const add = (candidate) => {
113
+ if (candidate.length === 0)
114
+ return;
115
+ if (!roots.includes(candidate))
116
+ roots.push(candidate);
117
+ };
118
+ add(options.gateRoot);
119
+ for (const root of options.systemRoots ?? [])
120
+ add(root);
121
+ for (const entry of options.declared ?? []) {
122
+ if (typeof entry !== "string" || entry.length === 0)
123
+ continue;
124
+ if (isUnreadableTarget(entry))
125
+ continue;
126
+ add(entry.startsWith("/") ? entry : `${options.gateRoot}/${entry}`);
127
+ }
128
+ return roots;
129
+ }
130
+ /** Which readers take paths, and where. Keyed by the binary's basename. */
131
+ export const READ_TARGET_SHAPES = {
132
+ cat: "all",
133
+ cksum: "all",
134
+ cut: "all",
135
+ diff: "all",
136
+ du: "all",
137
+ file: "all",
138
+ find: "walk",
139
+ grep: "after-pattern",
140
+ head: "all",
141
+ jq: "after-pattern",
142
+ ls: "all",
143
+ md5sum: "all",
144
+ rg: "after-pattern",
145
+ sed: "after-pattern",
146
+ sha256sum: "all",
147
+ shasum: "all",
148
+ sort: "all",
149
+ stat: "all",
150
+ tail: "all",
151
+ tree: "all",
152
+ uniq: "all",
153
+ wc: "all",
154
+ };
155
+ /**
156
+ * `find` primaries: the first word starting with `-` ends the path list.
157
+ *
158
+ * `find` is the one reader whose arguments are a little language, and its shape
159
+ * is `find [paths…] [expression]`. This one reads the RAW argument list rather
160
+ * than the flag-filtered positionals, because the filter is what tells the
161
+ * paths from the expression: in `find . -name '*.ts'` the pattern `*.ts` is a
162
+ * positional too, and a rule fed the filtered list would read it as a path,
163
+ * find it unreadable, and call an ordinary walk of the workspace out of scope.
164
+ */
165
+ function walkTargets(args) {
166
+ const targets = [];
167
+ for (const word of args) {
168
+ if (word.startsWith("-"))
169
+ break;
170
+ targets.push(word);
171
+ }
172
+ return targets;
173
+ }
174
+ /**
175
+ * Flags that carry the pattern or the script, so every positional is a path.
176
+ *
177
+ * `grep -e needle src`, `sed -f script.sed file`, `rg --regexp needle dir`: the
178
+ * first positional is the FILE, and a rule that skipped it would leave the one
179
+ * target that matters unchecked. Under-detection is the failure mode that
180
+ * matters here — an unchecked read is a read outside the jail — so the shape
181
+ * widens to `all` whenever one of these appears.
182
+ */
183
+ const PATTERN_BEARING_FLAGS = [
184
+ "-e",
185
+ "-f",
186
+ "--regexp",
187
+ "--expression",
188
+ "--file",
189
+ ];
190
+ /** Did the pattern (or script) arrive through a flag rather than a positional? */
191
+ function patternCameFromFlag(args) {
192
+ return args.some((arg) => PATTERN_BEARING_FLAGS.includes(arg) ||
193
+ PATTERN_BEARING_FLAGS.some((flag) => flag.startsWith("--") && arg.startsWith(`${flag}=`)));
194
+ }
195
+ /**
196
+ * The paths a read command will open, or `null` when this binary is not one
197
+ * whose reads this module scopes.
198
+ *
199
+ * An empty array is a real answer and is NOT the same as `null`: it means this
200
+ * reader opens the working directory (`ls`, `find`, `grep needle` with no file
201
+ * operand), and the caller checks the working directory in its place. `null`
202
+ * means "not a scoped reader", and the caller leaves the segment alone.
203
+ *
204
+ * Treating a non-path positional as a path costs nothing: a bare word resolves
205
+ * against the working directory, which is inside a root in every session this
206
+ * module is meant for. Treating a path as a non-path costs the whole property.
207
+ */
208
+ export function readTargetsOf(bin, positionals, args = []) {
209
+ const shape = READ_TARGET_SHAPES[basenameOf(bin)];
210
+ if (shape === undefined)
211
+ return null;
212
+ switch (shape) {
213
+ case "all":
214
+ return [...positionals];
215
+ case "after-pattern":
216
+ return patternCameFromFlag(args) ? [...positionals] : positionals.slice(1);
217
+ case "walk":
218
+ return walkTargets(args.length === 0 ? positionals : args);
219
+ }
220
+ }
221
+ /** The last segment of a command word, so `/usr/bin/cat` reads as `cat`. */
222
+ function basenameOf(bin) {
223
+ const segments = segmentsOf(bin);
224
+ return segments[segments.length - 1] ?? bin;
225
+ }
226
+ /**
227
+ * Read one target against the roots, as far as text alone can settle it.
228
+ *
229
+ * Absolute and inside a root: in scope. Absolute and outside every root: out of
230
+ * scope, decided here, no disk needed. Unreadable (a variable, a glob, a `~`):
231
+ * out of scope, because what it names is not in the text. Anything relative, or
232
+ * carrying a `..`, is `needs-disk`.
233
+ */
234
+ export function readTargetVerdict(target, roots) {
235
+ if (target.length === 0)
236
+ return "needs-disk";
237
+ if (isUnreadableTarget(target))
238
+ return "out-of-scope";
239
+ if (!target.startsWith("/"))
240
+ return "needs-disk";
241
+ if (segmentsOf(target).includes(".."))
242
+ return "needs-disk";
243
+ return isInReadScope(target, roots) ? "in-scope" : "out-of-scope";
244
+ }
245
+ /**
246
+ * The roots, rendered for a human: `approval policy check`'s line and the
247
+ * hook's verdict note say the same sentence.
248
+ */
249
+ export function renderReadRoots(roots) {
250
+ return roots.length === 0 ? "(none)" : roots.join(", ");
251
+ }
252
+ //# sourceMappingURL=read-scope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-scope.js","sourceRoot":"","sources":["../../../src/core/read-scope.ts"],"names":[],"mappings":"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"}
@@ -153,9 +153,90 @@ export interface EgressAllowance {
153
153
  * reach the profile. Directories deny their whole subtree.
154
154
  */
155
155
  readonly denyRead: readonly string[];
156
+ /**
157
+ * WRITE CONFINEMENT (APRV-325.3): the only absolute subtrees the child may
158
+ * write, or `undefined` for the ordinary egress-only profile.
159
+ *
160
+ * This flips the posture for one rule family and one only. The header explains
161
+ * why the rest of this module is a deny-LIST: a `(deny default)` profile
162
+ * spends itself re-allowing dyld, the process's own binary and every temporary
163
+ * directory, and a control that breaks ordinary development is a control that
164
+ * gets switched off. That reasoning holds for reads and for everything else,
165
+ * and it does NOT hold for writes in a confined session, where the whole
166
+ * property being enforced is "this shell cannot change the canonical
167
+ * workspace". A write deny-list would have to enumerate every path worth
168
+ * protecting; this names the handful worth writing, so a path nobody thought
169
+ * of is denied rather than forgotten.
170
+ *
171
+ * `/dev` is allowed alongside them, unconditionally. Writes there are process
172
+ * I/O rather than filesystem state — `/dev/null`, `/dev/stdout`, `/dev/tty`,
173
+ * the pty a shell needs — and denying them kills the child before it can
174
+ * demonstrate anything, which is the failure mode point 1 of the header
175
+ * records for `network-outbound` and unix sockets.
176
+ *
177
+ * An EMPTY array is meaningful and is not the same as `undefined`: it denies
178
+ * every write outside `/dev`. `undefined` emits no write rules at all.
179
+ */
180
+ readonly writeAllow?: readonly string[];
181
+ /**
182
+ * The READ JAIL (APRV-347): absolute directories the child may read, with
183
+ * everything else denied.
184
+ *
185
+ * Absent or empty leaves the profile exactly as it was — allow-default with a
186
+ * deny-list — and the bytes are identical to the pre-APRV-347 profile, which
187
+ * a fixture test pins. That default is deliberate: a deny-default read
188
+ * profile is a much larger change to what ordinary development can do, and a
189
+ * control that breaks `npm run build` is a control someone switches off.
190
+ *
191
+ * When it IS set, the profile flips to `(deny file-read*)` plus a `subpath`
192
+ * allow for each of these roots and for the fixed runtime set a process needs
193
+ * to exist at all (see {@link RUNTIME_READ_PATHS}). `denyRead` still applies
194
+ * and is emitted AFTER the allows, so a credential file inside an allowed
195
+ * root stays unreadable: last match wins in SBPL, and the credential denial
196
+ * must be the last word.
197
+ */
198
+ readonly allowRead?: readonly string[];
156
199
  }
157
200
  /** The default: nothing allowed, nothing denied beyond the network. */
158
201
  export declare const DENY_ALL_EGRESS: EgressAllowance;
202
+ /**
203
+ * What a process must be able to read to exist, regardless of the jail.
204
+ *
205
+ * Deliberately a fixed compiled-in list rather than anything a caller supplies:
206
+ * a jail whose runtime set could be widened by the party under oversight is not
207
+ * a jail (SPEC.md §11.1, self-reported fields never reduce scrutiny).
208
+ *
209
+ * - `/usr/lib`, `/usr/share`, `/System`, `/Library` — dyld, the shared cache,
210
+ * ICU data and the frameworks every Mach-O binary links. A profile without
211
+ * them kills the process before `main`, which reads as the command failing
212
+ * rather than as the sandbox working.
213
+ * - `/private/var/db/dyld` and `/var/db/dyld` — the dyld shared cache, which
214
+ * moved out of `/usr/lib` and is opened by name.
215
+ * - `/dev` — `/dev/null`, `/dev/urandom`, `/dev/dtracehelper`, the tty.
216
+ * - `/bin`, `/usr/bin`, `/sbin`, `/usr/sbin`, `/opt/homebrew`, `/usr/local` —
217
+ * the interpreters and tools a build shells out to. The node binary's own
218
+ * realpath is added separately by {@link runtimeReadRoots}, because a Node
219
+ * installed by a version manager lives under the user's home and none of
220
+ * these covers it.
221
+ * - `/etc`, `/private/etc` — `resolv.conf`, `passwd`, the locale tables.
222
+ *
223
+ * `node_modules` is NOT here, and that is a stated limit rather than an
224
+ * oversight: a repository's dependencies live inside the repository, so they
225
+ * are covered by the gate root being a read root. A project whose dependencies
226
+ * sit OUTSIDE the root (a pnpm store elsewhere, a linked package) must name
227
+ * that directory in `read_scope.roots`, which is exactly the widening that key
228
+ * exists for. docs/sandboxed-exec.md says so beside the survey.
229
+ *
230
+ * **The temp roots are not here either**, and that one is load-bearing. They
231
+ * reach the profile through the CALLER — `resolveReadRoots` puts the session
232
+ * scratchpad and the system temp root in the effective read scope, so
233
+ * `approval run` and `approval sandbox` open them and build tooling keeps
234
+ * working — and a caller that passes narrower roots gets a narrower jail. A
235
+ * temp root compiled in here would have been a widening no operator could turn
236
+ * off and no test could demonstrate a denial against, which is how it was
237
+ * found.
238
+ */
239
+ export declare const RUNTIME_READ_PATHS: readonly string[];
159
240
  /**
160
241
  * The files beside a log that hold credential material, for the profile's
161
242
  * `denyRead`.