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.
- package/README.md +63 -24
- package/SPEC.md +57 -11
- package/dist/src/channels/contract.d.ts +34 -1
- package/dist/src/channels/contract.js +200 -7
- package/dist/src/channels/contract.js.map +1 -1
- package/dist/src/channels/telegram.d.ts +123 -11
- package/dist/src/channels/telegram.js +218 -23
- package/dist/src/channels/telegram.js.map +1 -1
- package/dist/src/channels/web.d.ts +9 -0
- package/dist/src/channels/web.js +17 -0
- package/dist/src/channels/web.js.map +1 -1
- package/dist/src/cli/amend.js +214 -30
- package/dist/src/cli/amend.js.map +1 -1
- package/dist/src/cli/attest.d.ts +9 -0
- package/dist/src/cli/attest.js +134 -7
- package/dist/src/cli/attest.js.map +1 -1
- package/dist/src/cli/channel-telegram.d.ts +99 -26
- package/dist/src/cli/channel-telegram.js +311 -13
- package/dist/src/cli/channel-telegram.js.map +1 -1
- package/dist/src/cli/channel.d.ts +9 -0
- package/dist/src/cli/channel.js +9 -0
- package/dist/src/cli/channel.js.map +1 -1
- package/dist/src/cli/codex-bridge.d.ts +819 -0
- package/dist/src/cli/codex-bridge.js +1607 -0
- package/dist/src/cli/codex-bridge.js.map +1 -0
- package/dist/src/cli/codex.d.ts +1 -1
- package/dist/src/cli/codex.js +304 -7
- package/dist/src/cli/codex.js.map +1 -1
- package/dist/src/cli/daemon.js +4 -1
- package/dist/src/cli/daemon.js.map +1 -1
- package/dist/src/cli/doctor.js +467 -12
- package/dist/src/cli/doctor.js.map +1 -1
- package/dist/src/cli/execute.js +25 -2
- package/dist/src/cli/execute.js.map +1 -1
- package/dist/src/cli/help.d.ts +6 -2
- package/dist/src/cli/help.js +165 -60
- package/dist/src/cli/help.js.map +1 -1
- package/dist/src/cli/hook-codex.d.ts +49 -1
- package/dist/src/cli/hook-codex.js +60 -1
- package/dist/src/cli/hook-codex.js.map +1 -1
- package/dist/src/cli/hook.d.ts +459 -3
- package/dist/src/cli/hook.js +1062 -114
- package/dist/src/cli/hook.js.map +1 -1
- package/dist/src/cli/import.js +1 -1
- package/dist/src/cli/import.js.map +1 -1
- package/dist/src/cli/main.js +5 -3
- package/dist/src/cli/main.js.map +1 -1
- package/dist/src/cli/policy-apply.d.ts +195 -0
- package/dist/src/cli/policy-apply.js +573 -0
- package/dist/src/cli/policy-apply.js.map +1 -0
- package/dist/src/cli/policy.js +14 -1
- package/dist/src/cli/policy.js.map +1 -1
- package/dist/src/cli/preflight.d.ts +151 -13
- package/dist/src/cli/preflight.js +398 -41
- package/dist/src/cli/preflight.js.map +1 -1
- package/dist/src/cli/sandbox.js +17 -1
- package/dist/src/cli/sandbox.js.map +1 -1
- package/dist/src/cli/scaffold.d.ts +1 -1
- package/dist/src/cli/scaffold.js +1 -1
- package/dist/src/cli/setup-channel.d.ts +9 -0
- package/dist/src/cli/setup-channel.js +28 -1
- package/dist/src/cli/setup-channel.js.map +1 -1
- package/dist/src/cli/setup-common.d.ts +3 -1
- package/dist/src/cli/setup-common.js +3 -2
- package/dist/src/cli/setup-common.js.map +1 -1
- package/dist/src/cli/setup.d.ts +2 -0
- package/dist/src/cli/setup.js +94 -2
- package/dist/src/cli/setup.js.map +1 -1
- package/dist/src/cli/up.js +115 -51
- package/dist/src/cli/up.js.map +1 -1
- package/dist/src/cli/values.js +3 -4
- package/dist/src/cli/values.js.map +1 -1
- package/dist/src/cli/verb-registry.js +174 -9
- package/dist/src/cli/verb-registry.js.map +1 -1
- package/dist/src/cli/wordmark.d.ts +2 -2
- package/dist/src/cli/wordmark.js +2 -2
- package/dist/src/codex/broker.d.ts +229 -0
- package/dist/src/codex/broker.js +548 -0
- package/dist/src/codex/broker.js.map +1 -0
- package/dist/src/codex/runner.d.ts +178 -0
- package/dist/src/codex/runner.js +231 -0
- package/dist/src/codex/runner.js.map +1 -0
- package/dist/src/codex/serve.d.ts +56 -0
- package/dist/src/codex/serve.js +98 -0
- package/dist/src/codex/serve.js.map +1 -0
- package/dist/src/codex/workspace-commit.d.ts +219 -0
- package/dist/src/codex/workspace-commit.js +549 -0
- package/dist/src/codex/workspace-commit.js.map +1 -0
- package/dist/src/core/advance-cycle.d.ts +51 -0
- package/dist/src/core/advance-cycle.js +66 -2
- package/dist/src/core/advance-cycle.js.map +1 -1
- package/dist/src/core/agents-md.d.ts +20 -18
- package/dist/src/core/agents-md.js +33 -31
- package/dist/src/core/agents-md.js.map +1 -1
- package/dist/src/core/attest.d.ts +215 -0
- package/dist/src/core/attest.js +317 -7
- package/dist/src/core/attest.js.map +1 -1
- package/dist/src/core/audit.d.ts +18 -0
- package/dist/src/core/audit.js +13 -0
- package/dist/src/core/audit.js.map +1 -1
- package/dist/src/core/channel-owner.d.ts +213 -0
- package/dist/src/core/channel-owner.js +358 -0
- package/dist/src/core/channel-owner.js.map +1 -0
- package/dist/src/core/command-class.d.ts +154 -0
- package/dist/src/core/command-class.js +673 -20
- package/dist/src/core/command-class.js.map +1 -1
- package/dist/src/core/commit-guard.d.ts +272 -0
- package/dist/src/core/commit-guard.js +424 -0
- package/dist/src/core/commit-guard.js.map +1 -0
- package/dist/src/core/daemon-actor.d.ts +45 -0
- package/dist/src/core/daemon-actor.js +54 -0
- package/dist/src/core/daemon-actor.js.map +1 -0
- package/dist/src/core/dark-session.d.ts +109 -8
- package/dist/src/core/dark-session.js +266 -82
- package/dist/src/core/dark-session.js.map +1 -1
- package/dist/src/core/decision-refusal.d.ts +23 -2
- package/dist/src/core/decision-refusal.js +24 -2
- package/dist/src/core/decision-refusal.js.map +1 -1
- package/dist/src/core/env-file.d.ts +5 -0
- package/dist/src/core/env-file.js +60 -1
- package/dist/src/core/env-file.js.map +1 -1
- package/dist/src/core/execute.d.ts +15 -2
- package/dist/src/core/execute.js +15 -2
- package/dist/src/core/execute.js.map +1 -1
- package/dist/src/core/gate.d.ts +86 -1
- package/dist/src/core/gate.js +81 -1
- package/dist/src/core/gate.js.map +1 -1
- package/dist/src/core/gesture-refusal.d.ts +166 -0
- package/dist/src/core/gesture-refusal.js +188 -0
- package/dist/src/core/gesture-refusal.js.map +1 -0
- package/dist/src/core/harness-version.d.ts +1 -1
- package/dist/src/core/harness-version.js +3 -1
- package/dist/src/core/harness-version.js.map +1 -1
- package/dist/src/core/instance.d.ts +59 -2
- package/dist/src/core/instance.js +113 -0
- package/dist/src/core/instance.js.map +1 -1
- package/dist/src/core/log.d.ts +39 -1
- package/dist/src/core/log.js.map +1 -1
- package/dist/src/core/policy-explain.d.ts +10 -0
- package/dist/src/core/policy-explain.js +32 -0
- package/dist/src/core/policy-explain.js.map +1 -1
- package/dist/src/core/policy-load.d.ts +41 -1
- package/dist/src/core/policy-load.js +21 -3
- package/dist/src/core/policy-load.js.map +1 -1
- package/dist/src/core/policy-match.d.ts +43 -0
- package/dist/src/core/policy-match.js +52 -0
- package/dist/src/core/policy-match.js.map +1 -1
- package/dist/src/core/policy-proposal.d.ts +52 -0
- package/dist/src/core/policy-proposal.js +102 -2
- package/dist/src/core/policy-proposal.js.map +1 -1
- package/dist/src/core/protected-path-guard.d.ts +117 -4
- package/dist/src/core/protected-path-guard.js +362 -48
- package/dist/src/core/protected-path-guard.js.map +1 -1
- package/dist/src/core/question-preempted.d.ts +141 -0
- package/dist/src/core/question-preempted.js +152 -0
- package/dist/src/core/question-preempted.js.map +1 -0
- package/dist/src/core/read-scope.d.ts +172 -0
- package/dist/src/core/read-scope.js +252 -0
- package/dist/src/core/read-scope.js.map +1 -0
- package/dist/src/core/sandbox.d.ts +81 -0
- package/dist/src/core/sandbox.js +190 -1
- package/dist/src/core/sandbox.js.map +1 -1
- package/dist/src/core/sender-identity.d.ts +476 -0
- package/dist/src/core/sender-identity.js +572 -0
- package/dist/src/core/sender-identity.js.map +1 -0
- package/dist/src/core/shlex.d.ts +102 -0
- package/dist/src/core/shlex.js +159 -0
- package/dist/src/core/shlex.js.map +1 -0
- package/dist/src/core/values.d.ts +18 -8
- package/dist/src/core/values.js +36 -1
- package/dist/src/core/values.js.map +1 -1
- package/dist/src/daemon/advance.d.ts +10 -0
- package/dist/src/daemon/advance.js +25 -4
- package/dist/src/daemon/advance.js.map +1 -1
- package/dist/src/daemon/daemon.js +9 -0
- package/dist/src/daemon/daemon.js.map +1 -1
- package/dist/src/daemon/git-evidence.d.ts +2 -2
- package/dist/src/daemon/git-evidence.js +1 -1
- package/dist/src/mcp/server.js +8 -0
- package/dist/src/mcp/server.js.map +1 -1
- package/docs/cli-reference.md +932 -32
- package/docs/codex-enforced-session.md +75 -2
- package/docs/codex-workspace-broker.md +118 -0
- package/package.json +3 -1
- package/schema/event.schema.json +538 -9
- package/schema/fixtures/event/invalid/approval-granted-sender-hashed-false.json +20 -0
- package/schema/fixtures/event/invalid/approval-granted-sender-hashed-raw-id.json +20 -0
- package/schema/fixtures/event/invalid/audit-gesture-refused-human-actor.json +16 -0
- package/schema/fixtures/event/invalid/audit-gesture-refused-no-actor-no-sender.json +15 -0
- package/schema/fixtures/event/invalid/audit-gesture-refused-unknown-gesture.json +16 -0
- package/schema/fixtures/event/invalid/audit-question-preempted-agent-actor.json +16 -0
- package/schema/fixtures/event/invalid/audit-question-preempted-no-question-id.json +16 -0
- package/schema/fixtures/event/invalid/audit-question-preempted-unknown-source.json +15 -0
- package/schema/fixtures/event/invalid/gate-path-signed-off-absolute-path.json +14 -0
- package/schema/fixtures/event/invalid/gate-path-signed-off-agent-actor.json +14 -0
- package/schema/fixtures/event/invalid/gate-path-signed-off-missing-path.json +13 -0
- package/schema/fixtures/event/valid/approval-granted-sender-hashed.json +20 -0
- package/schema/fixtures/event/valid/audit-gesture-refused-review-note.json +21 -0
- package/schema/fixtures/event/valid/audit-gesture-refused-sender-key-unavailable.json +19 -0
- package/schema/fixtures/event/valid/audit-gesture-refused.json +19 -0
- package/schema/fixtures/event/valid/audit-question-preempted-no-verdict.json +16 -0
- package/schema/fixtures/event/valid/audit-question-preempted.json +20 -0
- package/schema/fixtures/event/valid/gate-path-signed-off.json +14 -0
- package/schema/fixtures/event/valid/harness-kind-claude-code.json +23 -0
- package/schema/fixtures/event/valid/harness-kind-codex.json +23 -0
- package/schema/fixtures/event/valid/harness-kind-cursor.json +23 -0
- package/schema/fixtures/event/valid/harness-kind-grok.json +23 -0
- package/schema/fixtures/event/valid/harness-kind-muse.json +23 -0
- package/schema/fixtures/policy/invalid/senders-half-keyed.json +20 -0
- package/schema/fixtures/policy/valid/canonical.json +1 -1
- package/schema/fixtures/policy/valid/senders-keyed.json +24 -0
- package/schema/fixtures/policy-md/valid/canonical.md +1 -1
- package/schema/fixtures/policy-md/valid/with-values.md +5 -7
- package/schema/fixtures/values/invalid/class-shaped.json +1 -1
- package/schema/fixtures/values/invalid/duplicate-entry.json +1 -1
- package/schema/fixtures/values/invalid/non-string-item.json +1 -1
- package/schema/fixtures/values/invalid/over-cap.json +1 -1
- package/schema/fixtures/values/invalid/unknown-key.json +1 -1
- package/schema/fixtures/values/invalid/version-float.json +1 -0
- package/schema/fixtures/values/invalid/version-integer.json +1 -0
- package/schema/fixtures/values/invalid/version-wrong-string.json +1 -0
- package/schema/fixtures/values/valid/empty-lists.json +2 -3
- package/schema/fixtures/values/valid/full.json +5 -7
- package/schema/fixtures/values/valid/minimal.json +1 -1
- package/schema/fixtures/values-md/invalid/schema-invalid.md +5 -3
- package/schema/fixtures/values-md/invalid/two-blocks.md +3 -3
- package/schema/fixtures/values-md/invalid/unterminated.md +2 -2
- package/schema/fixtures/values-md/invalid/version-1.md +69 -0
- package/schema/fixtures/values-md/invalid/version-unquoted.md +64 -0
- package/schema/fixtures/values-md/invalid/yaml-error.md +2 -2
- package/schema/fixtures/values-md/valid/absent.md +1 -1
- package/schema/fixtures/values-md/valid/with-values.md +5 -7
- package/schema/policy.schema.json +54 -2
- package/schema/values.schema.json +7 -11
- 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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAEH;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,wBAAwB,CAAC;AAchE,+EAA+E;AAC/E,SAAS,UAAU,CAAC,SAAiB;IACnC,OAAO,SAAS;SACb,KAAK,CAAC,SAAS,CAAC;SAChB,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,OAAO,KAAK,GAAG,CAAC,CAAC;AAChE,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,mBAAmB,CAAC,SAAiB,EAAE,IAAY;IACjE,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;IAC9B,MAAM,IAAI,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC;IACnC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACpC,IAAI,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IAC5C,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE,CAAC,OAAO,KAAK,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;AACjE,CAAC;AAED,8CAA8C;AAC9C,MAAM,UAAU,aAAa,CAAC,SAAiB,EAAE,KAAwB;IACvE,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,mBAAmB,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC,CAAC;AACpE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,OAAO,CACL,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;QAClB,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;QAClB,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;QAClB,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;QAClB,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CACrB,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAIlC;IACC,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,GAAG,GAAG,CAAC,SAAiB,EAAQ,EAAE;QACtC,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QACnC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,SAAS,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACxD,CAAC,CAAC;IACF,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACtB,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,WAAW,IAAI,EAAE;QAAE,GAAG,CAAC,IAAI,CAAC,CAAC;IACxD,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,QAAQ,IAAI,EAAE,EAAE,CAAC;QAC3C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QAC9D,IAAI,kBAAkB,CAAC,KAAK,CAAC;YAAE,SAAS;QACxC,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,QAAQ,IAAI,KAAK,EAAE,CAAC,CAAC;IACtE,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAoCD,2EAA2E;AAC3E,MAAM,CAAC,MAAM,kBAAkB,GAA8C;IAC3E,GAAG,EAAE,KAAK;IACV,KAAK,EAAE,KAAK;IACZ,GAAG,EAAE,KAAK;IACV,IAAI,EAAE,KAAK;IACX,EAAE,EAAE,KAAK;IACT,IAAI,EAAE,KAAK;IACX,IAAI,EAAE,MAAM;IACZ,IAAI,EAAE,eAAe;IACrB,IAAI,EAAE,KAAK;IACX,EAAE,EAAE,eAAe;IACnB,EAAE,EAAE,KAAK;IACT,MAAM,EAAE,KAAK;IACb,EAAE,EAAE,eAAe;IACnB,GAAG,EAAE,eAAe;IACpB,SAAS,EAAE,KAAK;IAChB,MAAM,EAAE,KAAK;IACb,IAAI,EAAE,KAAK;IACX,IAAI,EAAE,KAAK;IACX,IAAI,EAAE,KAAK;IACX,IAAI,EAAE,KAAK;IACX,IAAI,EAAE,KAAK;IACX,EAAE,EAAE,KAAK;CACV,CAAC;AAEF;;;;;;;;;GASG;AACH,SAAS,WAAW,CAAC,IAAuB;IAC1C,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE,CAAC;QACxB,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,MAAM;QAChC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACrB,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,qBAAqB,GAAsB;IAC/C,IAAI;IACJ,IAAI;IACJ,UAAU;IACV,cAAc;IACd,QAAQ;CACT,CAAC;AAEF,kFAAkF;AAClF,SAAS,mBAAmB,CAAC,IAAuB;IAClD,OAAO,IAAI,CAAC,IAAI,CACd,CAAC,GAAG,EAAE,EAAE,CACN,qBAAqB,CAAC,QAAQ,CAAC,GAAG,CAAC;QACnC,qBAAqB,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,CAC5F,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,aAAa,CAC3B,GAAW,EACX,WAA8B,EAC9B,IAAI,GAAsB,EAAE;IAE5B,MAAM,KAAK,GAAG,kBAAkB,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;IAClD,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IACrC,QAAQ,KAAK,EAAE,CAAC;QACd,KAAK,KAAK;YACR,OAAO,CAAC,GAAG,WAAW,CAAC,CAAC;QAC1B,KAAK,eAAe;YAClB,OAAO,mBAAmB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAC7E,KAAK,MAAM;YACT,OAAO,WAAW,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IAC/D,CAAC;AACH,CAAC;AAED,4EAA4E;AAC5E,SAAS,UAAU,CAAC,GAAW;IAC7B,MAAM,QAAQ,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC;IACjC,OAAO,QAAQ,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,GAAG,CAAC;AAC9C,CAAC;AAaD;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAC/B,MAAc,EACd,KAAwB;IAExB,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,YAAY,CAAC;IAC7C,IAAI,kBAAkB,CAAC,MAAM,CAAC;QAAE,OAAO,cAAc,CAAC;IACtD,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,YAAY,CAAC;IACjD,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,YAAY,CAAC;IAC3D,OAAO,aAAa,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,cAAc,CAAC;AACpE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,KAAwB;IACtD,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1D,CAAC"}
|
|
@@ -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`.
|