approval-md 0.1.0 → 0.2.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 +584 -553
- package/SPEC.md +42 -13
- package/dist/src/adapters/agentmail.d.ts +426 -0
- package/dist/src/adapters/agentmail.js +2 -2
- package/dist/src/adapters/conformance.d.ts +149 -0
- package/dist/src/adapters/contract.d.ts +628 -0
- package/dist/src/adapters/contract.js +110 -16
- package/dist/src/adapters/contract.js.map +1 -1
- package/dist/src/adapters/email.d.ts +324 -0
- package/dist/src/adapters/env-passphrase.d.ts +93 -0
- package/dist/src/adapters/public.d.ts +11 -0
- package/dist/src/adapters/public.js +11 -0
- package/dist/src/adapters/public.js.map +1 -0
- package/dist/src/adapters/registry.d.ts +59 -0
- package/dist/src/adapters/registry.js +2 -1
- package/dist/src/adapters/registry.js.map +1 -1
- package/dist/src/adapters/smtp.d.ts +213 -0
- package/dist/src/adapters/vault-provider.d.ts +114 -0
- package/dist/src/adapters/vault-provider.js +3 -3
- package/dist/src/adapters/zzz.d.ts +66 -0
- package/dist/src/adapters/zzz.js +299 -0
- package/dist/src/adapters/zzz.js.map +1 -0
- package/dist/src/channels/batch.d.ts +109 -0
- package/dist/src/channels/cli.d.ts +193 -0
- package/dist/src/channels/conformance.d.ts +92 -0
- package/dist/src/channels/contract.d.ts +623 -0
- package/dist/src/channels/payload-view.d.ts +35 -0
- package/dist/src/channels/render-queue.d.ts +149 -0
- package/dist/src/channels/tagging.d.ts +196 -0
- package/dist/src/channels/telegram.d.ts +1832 -0
- package/dist/src/channels/web.d.ts +341 -0
- package/dist/src/cli/adapter.d.ts +90 -0
- package/dist/src/cli/adapter.js +25 -15
- package/dist/src/cli/adapter.js.map +1 -1
- package/dist/src/cli/amend.d.ts +59 -0
- package/dist/src/cli/args.d.ts +43 -0
- package/dist/src/cli/attest.d.ts +41 -0
- package/dist/src/cli/audit-card.d.ts +62 -0
- package/dist/src/cli/audit.d.ts +59 -0
- package/dist/src/cli/channel-telegram.d.ts +806 -0
- package/dist/src/cli/channel-web.d.ts +131 -0
- package/dist/src/cli/channel.d.ts +71 -0
- package/dist/src/cli/checkpoint-tap.d.ts +169 -0
- package/dist/src/cli/codex.d.ts +2 -0
- package/dist/src/cli/codex.js +172 -0
- package/dist/src/cli/codex.js.map +1 -0
- package/dist/src/cli/coverage.d.ts +61 -0
- package/dist/src/cli/daemon.d.ts +120 -0
- package/dist/src/cli/doctor.d.ts +129 -0
- package/dist/src/cli/doctor.js +119 -5
- package/dist/src/cli/doctor.js.map +1 -1
- package/dist/src/cli/env.d.ts +65 -0
- package/dist/src/cli/execute.d.ts +202 -0
- package/dist/src/cli/exit-codes.d.ts +73 -0
- package/dist/src/cli/feedback.d.ts +60 -0
- package/dist/src/cli/gate-window.d.ts +40 -0
- package/dist/src/cli/gate.d.ts +68 -0
- package/dist/src/cli/git-scope.d.ts +190 -0
- package/dist/src/cli/gloss-attach.d.ts +85 -0
- package/dist/src/cli/gloss-codex-child.d.ts +9 -0
- package/dist/src/cli/gloss-codex.d.ts +24 -0
- package/dist/src/cli/gloss-options.d.ts +42 -0
- package/dist/src/cli/gloss.d.ts +265 -0
- package/dist/src/cli/help.d.ts +103 -0
- package/dist/src/cli/help.js +173 -51
- package/dist/src/cli/help.js.map +1 -1
- package/dist/src/cli/hook-codex.d.ts +78 -0
- package/dist/src/cli/hook-codex.js +167 -0
- package/dist/src/cli/hook-codex.js.map +1 -0
- package/dist/src/cli/hook.d.ts +331 -0
- package/dist/src/cli/hook.js +186 -80
- package/dist/src/cli/hook.js.map +1 -1
- package/dist/src/cli/import.d.ts +35 -0
- package/dist/src/cli/init.d.ts +84 -0
- package/dist/src/cli/init.js +2 -2
- package/dist/src/cli/init.js.map +1 -1
- package/dist/src/cli/instructions.d.ts +23 -0
- package/dist/src/cli/journal.d.ts +41 -0
- package/dist/src/cli/log-advance.d.ts +287 -0
- package/dist/src/cli/log-advance.js +102 -11
- package/dist/src/cli/log-advance.js.map +1 -1
- package/dist/src/cli/log-anchor.d.ts +176 -0
- package/dist/src/cli/log-checkpoint.d.ts +22 -0
- package/dist/src/cli/log-sync.d.ts +243 -0
- package/dist/src/cli/log-verbs.d.ts +16 -0
- package/dist/src/cli/log-verbs.js +7 -1
- package/dist/src/cli/log-verbs.js.map +1 -1
- package/dist/src/cli/long-help.d.ts +70 -0
- package/dist/src/cli/main.d.ts +77 -0
- package/dist/src/cli/main.js +155 -5
- package/dist/src/cli/main.js.map +1 -1
- package/dist/src/cli/mcp.d.ts +52 -0
- package/dist/src/cli/paths.d.ts +56 -0
- package/dist/src/cli/payload.d.ts +58 -0
- package/dist/src/cli/policy.d.ts +43 -0
- package/dist/src/cli/preflight.d.ts +363 -0
- package/dist/src/cli/preflight.js +294 -7
- package/dist/src/cli/preflight.js.map +1 -1
- package/dist/src/cli/progress.d.ts +78 -0
- package/dist/src/cli/prompt.d.ts +209 -0
- package/dist/src/cli/quickstart.d.ts +46 -0
- package/dist/src/cli/quickstart.js +297 -0
- package/dist/src/cli/quickstart.js.map +1 -0
- package/dist/src/cli/records.d.ts +34 -0
- package/dist/src/cli/render.d.ts +22 -0
- package/dist/src/cli/sandbox.d.ts +51 -0
- package/dist/src/cli/scaffold.d.ts +79 -0
- package/dist/src/cli/setup-adapter.d.ts +137 -0
- package/dist/src/cli/setup-adapter.js +38 -4
- package/dist/src/cli/setup-adapter.js.map +1 -1
- package/dist/src/cli/setup-channel.d.ts +117 -0
- package/dist/src/cli/setup-checkpoint.d.ts +57 -0
- package/dist/src/cli/setup-common.d.ts +275 -0
- package/dist/src/cli/setup-flow.d.ts +287 -0
- package/dist/src/cli/setup-service.d.ts +96 -0
- package/dist/src/cli/setup.d.ts +202 -0
- package/dist/src/cli/style.d.ts +320 -0
- package/dist/src/cli/token.d.ts +39 -0
- package/dist/src/cli/up.d.ts +155 -0
- package/dist/src/cli/up.js +4 -2
- package/dist/src/cli/up.js.map +1 -1
- package/dist/src/cli/usage.d.ts +37 -0
- package/dist/src/cli/values.d.ts +40 -0
- package/dist/src/cli/vault.d.ts +59 -0
- package/dist/src/cli/vault.js +2 -2
- package/dist/src/cli/vault.js.map +1 -1
- package/dist/src/cli/verb-registry.d.ts +76 -0
- package/dist/src/cli/verb-registry.js +176 -8
- package/dist/src/cli/verb-registry.js.map +1 -1
- package/dist/src/cli/wordmark.d.ts +31 -0
- package/dist/src/cli/wordmark.js +2 -2
- package/dist/src/codex/doctor.d.ts +13 -0
- package/dist/src/codex/doctor.js +41 -0
- package/dist/src/codex/doctor.js.map +1 -0
- package/dist/src/codex/manifest.d.ts +49 -0
- package/dist/src/codex/manifest.js +103 -0
- package/dist/src/codex/manifest.js.map +1 -0
- package/dist/src/codex/templates.d.ts +41 -0
- package/dist/src/codex/templates.js +319 -0
- package/dist/src/codex/templates.js.map +1 -0
- package/dist/src/codex/trust.d.ts +19 -0
- package/dist/src/codex/trust.js +183 -0
- package/dist/src/codex/trust.js.map +1 -0
- package/dist/src/codex/workspace-plan.d.ts +131 -0
- package/dist/src/codex/workspace-plan.js +561 -0
- package/dist/src/codex/workspace-plan.js.map +1 -0
- package/dist/src/core/actor.d.ts +2 -0
- package/dist/src/core/actor.js +5 -0
- package/dist/src/core/actor.js.map +1 -0
- package/dist/src/core/advance-cycle.d.ts +170 -0
- package/dist/src/core/agents-md.d.ts +276 -0
- package/dist/src/core/apply-patch.d.ts +49 -0
- package/dist/src/core/apply-patch.js +266 -0
- package/dist/src/core/apply-patch.js.map +1 -0
- package/dist/src/core/attest.d.ts +420 -0
- package/dist/src/core/attest.js +13 -1
- package/dist/src/core/attest.js.map +1 -1
- package/dist/src/core/audit.d.ts +492 -0
- package/dist/src/core/budgets.d.ts +238 -0
- package/dist/src/core/checkpoint.d.ts +500 -0
- package/dist/src/core/child-env.d.ts +88 -0
- package/dist/src/core/clock.d.ts +52 -0
- package/dist/src/core/command-class.d.ts +543 -0
- package/dist/src/core/command-class.js +43 -8
- package/dist/src/core/command-class.js.map +1 -1
- package/dist/src/core/coverage-sources/adapter.d.ts +40 -0
- package/dist/src/core/coverage-sources/gh.d.ts +48 -0
- package/dist/src/core/coverage-sources/git.d.ts +101 -0
- package/dist/src/core/coverage.d.ts +217 -0
- package/dist/src/core/credential-spec.d.ts +72 -0
- package/dist/src/core/dark-session.d.ts +331 -0
- package/dist/src/core/decision-refusal.d.ts +185 -0
- package/dist/src/core/env-file.d.ts +450 -0
- package/dist/src/core/execute.d.ts +858 -0
- package/dist/src/core/execute.js +44 -6
- package/dist/src/core/execute.js.map +1 -1
- package/dist/src/core/frontmatter.d.ts +78 -0
- package/dist/src/core/gate-window.d.ts +312 -0
- package/dist/src/core/gate.d.ts +1364 -0
- package/dist/src/core/gate.js +68 -13
- package/dist/src/core/gate.js.map +1 -1
- package/dist/src/core/git-run.d.ts +73 -0
- package/dist/src/core/harness-version.d.ts +157 -0
- package/dist/src/core/harness-version.js +2 -1
- package/dist/src/core/harness-version.js.map +1 -1
- package/dist/src/core/harness-wait.d.ts +55 -0
- package/dist/src/core/head-retry.d.ts +107 -0
- package/dist/src/core/instance.d.ts +253 -0
- package/dist/src/core/intake-limits.d.ts +247 -0
- package/dist/src/core/jcs.d.ts +52 -0
- package/dist/src/core/journal.d.ts +144 -0
- package/dist/src/core/live-draw.d.ts +436 -0
- package/dist/src/core/log-reconcile.d.ts +89 -0
- package/dist/src/core/log-subscribe.d.ts +36 -0
- package/dist/src/core/log-subscribe.js +162 -0
- package/dist/src/core/log-subscribe.js.map +1 -0
- package/dist/src/core/log.d.ts +278 -0
- package/dist/src/core/loop.d.ts +274 -0
- package/dist/src/core/loop.js +11 -0
- package/dist/src/core/loop.js.map +1 -1
- package/dist/src/core/md-fence.d.ts +41 -0
- package/dist/src/core/money.d.ts +147 -0
- package/dist/src/core/payload-census.d.ts +74 -0
- package/dist/src/core/payload-store.d.ts +175 -0
- package/dist/src/core/payload.d.ts +71 -0
- package/dist/src/core/policy-diff.d.ts +292 -0
- package/dist/src/core/policy-diff.js +27 -4
- package/dist/src/core/policy-diff.js.map +1 -1
- package/dist/src/core/policy-expectations.d.ts +199 -0
- package/dist/src/core/policy-explain.d.ts +150 -0
- package/dist/src/core/policy-explain.js +31 -3
- package/dist/src/core/policy-explain.js.map +1 -1
- package/dist/src/core/policy-load.d.ts +527 -0
- package/dist/src/core/policy-load.js +15 -3
- package/dist/src/core/policy-load.js.map +1 -1
- package/dist/src/core/policy-match.d.ts +281 -0
- package/dist/src/core/policy-match.js +20 -9
- package/dist/src/core/policy-match.js.map +1 -1
- package/dist/src/core/policy-proposal.d.ts +265 -0
- package/dist/src/core/prompt-layout.d.ts +221 -0
- package/dist/src/core/protected-path-guard.d.ts +453 -0
- package/dist/src/core/protected-path-guard.js +514 -35
- package/dist/src/core/protected-path-guard.js.map +1 -1
- package/dist/src/core/registration.d.ts +25 -0
- package/dist/src/core/reindex.d.ts +99 -0
- package/dist/src/core/sampler.d.ts +313 -0
- package/dist/src/core/sandbox.d.ts +290 -0
- package/dist/src/core/seal.d.ts +165 -0
- package/dist/src/core/state.d.ts +505 -0
- package/dist/src/core/task-file.d.ts +185 -0
- package/dist/src/core/telegram-config.d.ts +93 -0
- package/dist/src/core/token.d.ts +409 -0
- package/dist/src/core/token.js +21 -38
- package/dist/src/core/token.js.map +1 -1
- package/dist/src/core/validate.d.ts +138 -0
- package/dist/src/core/values.d.ts +137 -0
- package/dist/src/core/vault.d.ts +291 -0
- package/dist/src/core/verified-snapshot.d.ts +204 -0
- package/dist/src/core/verify.d.ts +336 -0
- package/dist/src/core/version.d.ts +8 -0
- package/dist/src/core/wysiwys.d.ts +370 -0
- package/dist/src/daemon/advance-child.d.ts +39 -0
- package/dist/src/daemon/advance.d.ts +466 -0
- package/dist/src/daemon/audit.d.ts +87 -0
- package/dist/src/daemon/daemon.d.ts +1180 -0
- package/dist/src/daemon/dark-session.d.ts +64 -0
- package/dist/src/daemon/draw-child.d.ts +36 -0
- package/dist/src/daemon/draw.d.ts +154 -0
- package/dist/src/daemon/git-evidence.d.ts +173 -0
- package/dist/src/daemon/git-evidence.js +1 -1
- package/dist/src/daemon/projection.d.ts +180 -0
- package/dist/src/daemon/prune.d.ts +207 -0
- package/dist/src/mcp/http.d.ts +113 -0
- package/dist/src/mcp/server.d.ts +265 -0
- package/dist/src/mcp/server.js +9 -1
- package/dist/src/mcp/server.js.map +1 -1
- package/docs/adapter-api.md +106 -0
- package/docs/cli-reference.md +389 -36
- package/docs/codex-enforced-session.md +30 -0
- package/package.json +12 -2
- package/schema/codex-instance.schema.json +82 -0
- package/schema/event.schema.json +2 -1
- package/schema/fixtures/codex-instance/invalid/unpinned-codex-version.json +40 -0
- package/schema/fixtures/codex-instance/valid/canonical.json +40 -0
- package/schema/policy.schema.json +21 -1
- package/templates/codex/README.md +9 -0
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell command classification: a pure, deterministic map from a command line
|
|
3
|
+
* to the SPEC.md §7 side-effect classes it would produce (APRV-82).
|
|
4
|
+
*
|
|
5
|
+
* This is the input half of the Claude Code PreToolUse hook. The harness hands
|
|
6
|
+
* us a command string it is about to run; policy speaks in classes; something
|
|
7
|
+
* has to translate. That translation is the reviewable artifact of this task, so
|
|
8
|
+
* it lives in one file, is data-driven, and is exhaustively fixture-tested.
|
|
9
|
+
*
|
|
10
|
+
* Three properties are load-bearing.
|
|
11
|
+
*
|
|
12
|
+
* **Pure.** No filesystem, no clock, no environment, no network. The same string
|
|
13
|
+
* always yields the same answer, which is what makes the fixture table a real
|
|
14
|
+
* specification rather than a sample of observed behaviour.
|
|
15
|
+
*
|
|
16
|
+
* **Fail closed, in three named ways.** A construct whose effect cannot be read
|
|
17
|
+
* off the text is `opaque` (`bash -c`, `eval`, backticks, a tainting command
|
|
18
|
+
* substitution). A binary the table does not know is `unclassified`. A string
|
|
19
|
+
* the tokenizer cannot even split is `unparseable`. All three are refusals, and
|
|
20
|
+
* the hook denies on each: the alternative, guessing a class for a command we do
|
|
21
|
+
* not understand, would let the least legible command lines through the widest
|
|
22
|
+
* gap. Ambiguity inside a construct we DO understand resolves to the stricter
|
|
23
|
+
* class (`git push` with no refspec is `vcs.push.main`, an `rm` path holding an
|
|
24
|
+
* unexpanded `$VAR` is `files.delete.out_of_scope`).
|
|
25
|
+
*
|
|
26
|
+
* **Best effort, and honest about it.** This is a classifier over shell text,
|
|
27
|
+
* not a shell. It is a speed bump against an agent doing the wrong thing by
|
|
28
|
+
* mistake, in the SPEC.md §11 sense; it is not scheming-robust, and nothing here
|
|
29
|
+
* claims otherwise. The hard boundary stays where SPEC.md §10.4 puts it: the
|
|
30
|
+
* credentials in the vault and the token an adapter demands.
|
|
31
|
+
*
|
|
32
|
+
* Self-reported text is never read. The hook passes the command only; the
|
|
33
|
+
* harness's `description` field is authored by the very agent being gated
|
|
34
|
+
* (SPEC.md §11.1: self-reported fields never reduce scrutiny).
|
|
35
|
+
*/
|
|
36
|
+
/** Why a command could not be classified. Each denies; none is a soft failure. */
|
|
37
|
+
export type ClassifierFailureCode = "unclassified" | "opaque" | "unparseable";
|
|
38
|
+
/** One command in a pipeline or list, with the class it resolved to. */
|
|
39
|
+
export interface ClassifiedSegment {
|
|
40
|
+
/** The segment's source text, as written. */
|
|
41
|
+
text: string;
|
|
42
|
+
/** The dotted side-effect class (SPEC.md §7). */
|
|
43
|
+
class: string;
|
|
44
|
+
/** Which rule decided it, for `hook classify` output and for tests. */
|
|
45
|
+
rule: string;
|
|
46
|
+
/**
|
|
47
|
+
* The protected path that selected the class, present only when one did
|
|
48
|
+
* (APRV-143).
|
|
49
|
+
*
|
|
50
|
+
* The protected classes (`policy.edit`, `policy.core`, `log.mutate`) are the
|
|
51
|
+
* classes a segment can take because of a *value* in it rather than because
|
|
52
|
+
* of its binary, and until this field the value was discarded the moment the
|
|
53
|
+
* rule fired: an approver was told the class and left to guess which of six
|
|
54
|
+
* arguments earned it. It is the word the classifier matched, verbatim, so a
|
|
55
|
+
* channel can name it without a second search of its own.
|
|
56
|
+
*/
|
|
57
|
+
path?: string;
|
|
58
|
+
/**
|
|
59
|
+
* The sandbox wrapper this segment runs inside, when the classifier unwrapped
|
|
60
|
+
* one (APRV-193). Absent means "not wrapped, as far as this classifier can
|
|
61
|
+
* tell", which is the direction every caller must fail in: the marker is only
|
|
62
|
+
* ever read to RELAX a requirement, so an early return that omits it costs a
|
|
63
|
+
* refusal and never an authorization.
|
|
64
|
+
*
|
|
65
|
+
* `runtime` is `approval sandbox -- …`, whose profile this runtime writes.
|
|
66
|
+
* `external` is a hand-written `sandbox-exec -f <profile> …`, which is
|
|
67
|
+
* classified honestly (by its inner argv) and trusted for nothing: the
|
|
68
|
+
* profile is the caller's, and a profile a caller wrote can allow anything.
|
|
69
|
+
*/
|
|
70
|
+
sandbox?: SandboxWrapper;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Who wrote the profile a wrapped command runs under (APRV-193).
|
|
74
|
+
*
|
|
75
|
+
* The distinction is the whole reason this is not a boolean. Classification
|
|
76
|
+
* treats the two identically — both are unwrapped to the inner argv, because a
|
|
77
|
+
* wrapper is a room and the class belongs to what runs in it. Enforcement does
|
|
78
|
+
* not: `APPROVAL_HOOK_REQUIRE_SANDBOX` is satisfied only by `runtime`, because
|
|
79
|
+
* `sandbox-exec -f /tmp/anything.sb` with a profile whose only line is
|
|
80
|
+
* `(allow default)` denies nothing at all, and a requirement a caller can meet
|
|
81
|
+
* by writing their own permission is not a requirement.
|
|
82
|
+
*/
|
|
83
|
+
export type SandboxWrapper = "runtime" | "external";
|
|
84
|
+
export type CommandClassification = {
|
|
85
|
+
ok: true;
|
|
86
|
+
segments: ClassifiedSegment[];
|
|
87
|
+
classes: string[];
|
|
88
|
+
} | {
|
|
89
|
+
ok: false;
|
|
90
|
+
code: ClassifierFailureCode;
|
|
91
|
+
/** The segment (or whole command) that could not be read. */
|
|
92
|
+
segment: string;
|
|
93
|
+
detail: string;
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* The pass-through pseudo-class for the gate's own CLI.
|
|
97
|
+
*
|
|
98
|
+
* `approval …` is already the enforcement path — gating it with itself would
|
|
99
|
+
* either deadlock (the hook waiting on a decision that `approval grant` cannot
|
|
100
|
+
* deliver) or recurse. The hook allows this class without touching the log; it
|
|
101
|
+
* is never written to any envelope and no policy rule should name it.
|
|
102
|
+
*/
|
|
103
|
+
export declare const GATE_SELF_CLASS = "gate.self";
|
|
104
|
+
/**
|
|
105
|
+
* The three classes a protected path can select (APRV-198).
|
|
106
|
+
*
|
|
107
|
+
* One class used to cover the whole protected surface, so a policy could not
|
|
108
|
+
* say "an agent may edit the prose that describes the gate, under sampling,
|
|
109
|
+
* and may never touch the gate itself". These are that sentence, in the order
|
|
110
|
+
* of decreasing consequence:
|
|
111
|
+
*
|
|
112
|
+
* - `log.mutate` — anything aimed at `.approval/log/`. The log is the truth;
|
|
113
|
+
* a write to it is not an edit of the rules, it is an edit of the record of
|
|
114
|
+
* what happened.
|
|
115
|
+
* - `policy.core` — the policy file itself and the rest of the gate's own
|
|
116
|
+
* directory (env, payload store, vault, keys, `QUEUE.md`), plus the harness
|
|
117
|
+
* files that install the hook. An agent that can write these can write
|
|
118
|
+
* itself out of the gate without the gate ever seeing it.
|
|
119
|
+
* - `policy.edit` — the prose and configuration ABOUT the gate: the agent
|
|
120
|
+
* instructions, CI and release configuration, and whatever paths the policy
|
|
121
|
+
* itself protects. Reviewable after the fact, and the only one of the three
|
|
122
|
+
* a policy can sensibly sample.
|
|
123
|
+
*/
|
|
124
|
+
export type ProtectedPathClass = "log.mutate" | "policy.core" | "policy.edit";
|
|
125
|
+
/**
|
|
126
|
+
* The `policy.edit` sub-class namespace a `protected_paths` entry may route to
|
|
127
|
+
* (APRV-266).
|
|
128
|
+
*
|
|
129
|
+
* One protected surface with one autonomy was the shape until now, so a policy
|
|
130
|
+
* that wanted its specification sampled at one tenth and its release workflows
|
|
131
|
+
* gated every time had to choose one of those numbers for both. A routed entry
|
|
132
|
+
* says which sub-class a path family takes, and the sub-class is an ordinary
|
|
133
|
+
* §7 class with an ordinary policy line, so each family gets its own autonomy
|
|
134
|
+
* and its own live rate without any new grammar in `classes`.
|
|
135
|
+
*
|
|
136
|
+
* The namespace is closed to ONE extra segment under `policy.edit` and nothing
|
|
137
|
+
* else. A policy may not route a path to `policy.core`, to `log.mutate`, or to
|
|
138
|
+
* any class outside this namespace: those are the gate's own organs and the
|
|
139
|
+
* record of what happened, and §11.1 invariant 9 reserves them — a policy
|
|
140
|
+
* widening its own protected surface is naming prose and configuration, and
|
|
141
|
+
* mints no authority over anything else. `policy.schema.json` enforces the
|
|
142
|
+
* shape; this pattern is the same rule where the matcher can see it.
|
|
143
|
+
*/
|
|
144
|
+
export declare const POLICY_EDIT_SUBCLASS: RegExp;
|
|
145
|
+
/**
|
|
146
|
+
* Sub-class names this spec reserves, with the meaning an implementation must
|
|
147
|
+
* give them (APRV-266).
|
|
148
|
+
*
|
|
149
|
+
* Reserved so that two policies written by two people mean the same thing by
|
|
150
|
+
* `policy.edit.ci`, and so a reader of somebody else's policy does not have to
|
|
151
|
+
* infer it. An author is free to mint their own word beside these — the pattern
|
|
152
|
+
* above admits any lowercase word — and a minted name carries only the meaning
|
|
153
|
+
* its own policy line gives it.
|
|
154
|
+
*/
|
|
155
|
+
export declare const RESERVED_POLICY_EDIT_SUBCLASSES: Readonly<Record<string, string>>;
|
|
156
|
+
/**
|
|
157
|
+
* One `protected_paths` entry: a bare path, or a path routed to a sub-class.
|
|
158
|
+
*
|
|
159
|
+
* The bare string is the APRV-107 spelling and keeps its meaning exactly —
|
|
160
|
+
* `policy.edit`, as though the object form did not exist. Nothing about a
|
|
161
|
+
* string-only policy changes by a byte, which is the compatibility rule this
|
|
162
|
+
* type exists to state: `approval hook classify` over such a policy answers
|
|
163
|
+
* today's class with today's rule name.
|
|
164
|
+
*/
|
|
165
|
+
export type ProtectedPathEntry = string | {
|
|
166
|
+
path: string;
|
|
167
|
+
class: string;
|
|
168
|
+
};
|
|
169
|
+
/**
|
|
170
|
+
* One entry of `policy.protected_paths`, pre-split (APRV-107).
|
|
171
|
+
*
|
|
172
|
+
* `directory` records the trailing `/` that distinguishes `design/` (a subtree)
|
|
173
|
+
* from `design` (a file named `design`).
|
|
174
|
+
*/
|
|
175
|
+
interface ProtectedEntry {
|
|
176
|
+
segments: string[];
|
|
177
|
+
directory: boolean;
|
|
178
|
+
/**
|
|
179
|
+
* The class this entry routes to (APRV-266). `null` for a bare string entry,
|
|
180
|
+
* which means `policy.edit` and is matched in the built-in `policy.edit`
|
|
181
|
+
* tier exactly where it always was.
|
|
182
|
+
*/
|
|
183
|
+
routed: string | null;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Read one `policy.protected_paths` entry, or `null` when it names nothing.
|
|
187
|
+
*
|
|
188
|
+
* The schema already rejects globs, absolute paths and `..` segments; this
|
|
189
|
+
* repeats the structural half of that check so a caller that skipped
|
|
190
|
+
* validation gets an entry that matches nothing rather than an entry that
|
|
191
|
+
* matches surprisingly. Pure, like everything else here: no resolution against
|
|
192
|
+
* a checkout, no disk.
|
|
193
|
+
*
|
|
194
|
+
* The same defensiveness covers the routed form (APRV-266): an object whose
|
|
195
|
+
* `class` is not a well-formed `policy.edit` sub-class matches NOTHING at all
|
|
196
|
+
* rather than falling back to `policy.edit`. A silent fallback would be the
|
|
197
|
+
* worst of the three available answers — the author would read their file and
|
|
198
|
+
* see a rate that is not the rate in force — and the loader refuses such a
|
|
199
|
+
* policy outright, so this branch is only ever reached by a caller that
|
|
200
|
+
* skipped validation.
|
|
201
|
+
*/
|
|
202
|
+
export declare function parseProtectedEntry(entry: ProtectedPathEntry): ProtectedEntry | null;
|
|
203
|
+
/**
|
|
204
|
+
* Which protected class does this path name, if any? (APRV-198.)
|
|
205
|
+
*
|
|
206
|
+
* Deliberately name-based rather than location-based: the hook runs in whatever
|
|
207
|
+
* directory the harness is in, and a classifier that resolved paths against a
|
|
208
|
+
* checkout would answer differently in a worktree than in the primary. A
|
|
209
|
+
* false positive here costs one approval prompt; a false negative costs the
|
|
210
|
+
* property the whole file exists to defend.
|
|
211
|
+
*
|
|
212
|
+
* **The check order IS the precedence.** A path is answered by the strictest
|
|
213
|
+
* surface it names, `log.mutate` first, then `policy.core`, then
|
|
214
|
+
* `policy.edit`: `.approval/log/events.jsonl` is a log write and not merely an
|
|
215
|
+
* approval-home write, and a `policy.protected_paths` entry that happens to
|
|
216
|
+
* name a built-in surface cannot demote it, because the built-ins are matched
|
|
217
|
+
* before the policy's own list is read.
|
|
218
|
+
*
|
|
219
|
+
* `extra` carries `policy.protected_paths` (APRV-107). It is strictly
|
|
220
|
+
* ADDITIVE: the built-in set above is protected whatever a policy says, so a
|
|
221
|
+
* policy can widen the protected surface and can never narrow it, and every
|
|
222
|
+
* path it adds in the APRV-107 bare-string spelling lands on `policy.edit` —
|
|
223
|
+
* the reviewable class — because a policy widening its own surface is naming
|
|
224
|
+
* prose and configuration, not minting authority over the gate's organs. Still
|
|
225
|
+
* pure: the caller loads the policy, this function only matches segments.
|
|
226
|
+
*
|
|
227
|
+
* ## Routed entries (APRV-266)
|
|
228
|
+
*
|
|
229
|
+
* An entry written as `{path, class}` answers with the class it names, and the
|
|
230
|
+
* routed tier sits between the built-in `policy.core` tier and the built-in
|
|
231
|
+
* `policy.edit` tier. That position is the whole of the routing rule:
|
|
232
|
+
*
|
|
233
|
+
* - It is BELOW `log.mutate` and `policy.core`, so a routing can never reach
|
|
234
|
+
* the log or the gate's own organs. `{path: .approval/, class:
|
|
235
|
+
* policy.edit.home}` matches nothing, because tier 2 answered first — which
|
|
236
|
+
* is invariant 9's "no verb minting authority" holding at the one place a
|
|
237
|
+
* policy could otherwise have reached past it. The loader refuses such an
|
|
238
|
+
* entry outright rather than letting it sit inert.
|
|
239
|
+
* - It is ABOVE the built-in `policy.edit` set, so a routing CAN re-label a
|
|
240
|
+
* built-in `policy.edit` path: `{path: .github/workflows/, class:
|
|
241
|
+
* policy.edit.ci}` is exactly the sentence a project wants to write. What
|
|
242
|
+
* stops that from being a demotion is not this function but the load-time
|
|
243
|
+
* floor in `policy-load.ts`, which refuses a policy whose routing would
|
|
244
|
+
* resolve a built-in path below what the `policy.edit` line itself resolves
|
|
245
|
+
* to. The classifier stays pure: it reports the class the policy named and
|
|
246
|
+
* judges no autonomy.
|
|
247
|
+
*
|
|
248
|
+
* Among several routed entries matching one path, the MOST SPECIFIC wins (most
|
|
249
|
+
* segments), and declaration order breaks a tie. Most-specific is what a
|
|
250
|
+
* carve-out means — `design/` routed one way and `design/frozen/` another — and
|
|
251
|
+
* a tie is two entries of equal depth both claiming one path, which is the
|
|
252
|
+
* author's own ambiguity and is resolved the only way a pure function can:
|
|
253
|
+
* by the order they wrote them in.
|
|
254
|
+
*
|
|
255
|
+
* A string-only `extra` cannot reach the routed tier at all, so a policy that
|
|
256
|
+
* has not adopted the object form classifies byte for byte as it did before.
|
|
257
|
+
*/
|
|
258
|
+
export declare function protectedPathClass(candidate: string, extra?: readonly ProtectedPathEntry[]): string | null;
|
|
259
|
+
/**
|
|
260
|
+
* The class a path takes from the BUILT-IN set alone, ignoring every policy
|
|
261
|
+
* entry (APRV-266).
|
|
262
|
+
*
|
|
263
|
+
* The load-time routing floor needs this and nothing else: "is the path this
|
|
264
|
+
* entry routes one the runtime protects on its own?" decides whether the floor
|
|
265
|
+
* applies to it, and asking {@link protectedPathClass} with the policy's own
|
|
266
|
+
* entries in hand would answer with the routing under test.
|
|
267
|
+
*/
|
|
268
|
+
export declare function builtinProtectedPathClass(candidate: string): ProtectedPathClass | null;
|
|
269
|
+
/**
|
|
270
|
+
* Does this path name something only a human may write?
|
|
271
|
+
*
|
|
272
|
+
* The boolean face of {@link protectedPathClass}, kept because two callers
|
|
273
|
+
* (`core/wysiwys.ts`'s protected-path view and the hook's file-tool gate) ask
|
|
274
|
+
* whether a path is protected at all before they ask which surface it is.
|
|
275
|
+
*/
|
|
276
|
+
export declare function isProtectedPath(candidate: string, extra?: readonly ProtectedPathEntry[]): boolean;
|
|
277
|
+
/**
|
|
278
|
+
* One path, in the spelling an organ attestation records (APRV-272).
|
|
279
|
+
*
|
|
280
|
+
* Segment-wise: separators collapse, `./` noise disappears, a trailing slash
|
|
281
|
+
* goes, and the result is joined with `/` whatever the caller's platform uses.
|
|
282
|
+
* Two spellings of one file therefore attest and match as one file, which is
|
|
283
|
+
* the property the guard needs, since git reports `.claude/settings.json` and a
|
|
284
|
+
* human at a terminal may type `./.claude/settings.json`.
|
|
285
|
+
*
|
|
286
|
+
* Pure and disk-free, like everything else in this file: it never resolves,
|
|
287
|
+
* never follows a link, and never asks whether the path exists.
|
|
288
|
+
*/
|
|
289
|
+
export declare function normalizePathSpelling(candidate: string): string;
|
|
290
|
+
/**
|
|
291
|
+
* Is this path one of the gate's ORGANS — a `policy.core` surface a human can
|
|
292
|
+
* attest by content (APRV-272)?
|
|
293
|
+
*
|
|
294
|
+
* The organs are the harness files that install the hook: `.claude/settings*`
|
|
295
|
+
* and Cursor's `hooks.json`, `hooks/` and `agents/`. They are `policy.core`
|
|
296
|
+
* because an agent that could write them could write itself out of the gate,
|
|
297
|
+
* and `policy.core` is human-only, so the gate mints no record for them at all
|
|
298
|
+
* — which is exactly why the protected-path guard could never pass a hand-made
|
|
299
|
+
* edit to one, and why {@link normalizePathSpelling}-keyed attestation is the
|
|
300
|
+
* evidence for them.
|
|
301
|
+
*
|
|
302
|
+
* Two `policy.core` surfaces are deliberately NOT organs, and both keep their
|
|
303
|
+
* own rules:
|
|
304
|
+
*
|
|
305
|
+
* - The policy file, which has had content attestation since APRV-15 and whose
|
|
306
|
+
* attestation the gate reads on every operation. An organ record must never
|
|
307
|
+
* be able to stand in for it.
|
|
308
|
+
* - Everything under the approval home (`.approval/`): the payload store, the
|
|
309
|
+
* vault, the keys, the environment map, the queue. Those are the human's own
|
|
310
|
+
* ceremony surface, and the log directory under them is `log.mutate`, which
|
|
311
|
+
* is stricter still.
|
|
312
|
+
*
|
|
313
|
+
* The question is asked of the BUILT-IN set alone. A policy may not route any
|
|
314
|
+
* path to `policy.core` (§11.1 invariant 9 and the `policy.edit.*` namespace
|
|
315
|
+
* close that), so consulting the policy's entries here could only ever widen
|
|
316
|
+
* the set of files a human may attest by a routing the classifier already
|
|
317
|
+
* refuses to honor.
|
|
318
|
+
*/
|
|
319
|
+
export declare function isGateOrganPath(candidate: string): boolean;
|
|
320
|
+
/**
|
|
321
|
+
* Environment variables whose NAME says they carry a secret.
|
|
322
|
+
*
|
|
323
|
+
* Prefix-matched, because the classifier reads command text and never an
|
|
324
|
+
* environment: it cannot know which `APPROVAL_*` holds a token, so it treats
|
|
325
|
+
* the family alike and lets the allowlist below carve out the runtime's own
|
|
326
|
+
* non-secret names. Erring wide costs one approval prompt.
|
|
327
|
+
*
|
|
328
|
+
* Exported since APRV-205: `core/child-env.ts` starves a spawned child of the
|
|
329
|
+
* same family, and two copies of this list would be one list that drifts.
|
|
330
|
+
*
|
|
331
|
+
* `AGENTMAIL_` joins the family with the AgentMail adapter (APRV-224). An
|
|
332
|
+
* AgentMail API key is a mailbox in one string, and the deployment the adapter
|
|
333
|
+
* assumes hands the agent a key that cannot send while the sending key waits in
|
|
334
|
+
* the vault (SPEC.md §10.4). A key of either half in a granted child's
|
|
335
|
+
* environment would undo that split, so the prefix is withheld like the rest.
|
|
336
|
+
* The adapter's own declared credentials are vault names (`agentmail.api_key`,
|
|
337
|
+
* `agentmail.inbox_id`), so nothing under this prefix passes through by
|
|
338
|
+
* declaration either.
|
|
339
|
+
*/
|
|
340
|
+
export declare const SECRET_ENV_PREFIXES: readonly string[];
|
|
341
|
+
/**
|
|
342
|
+
* The runtime's own variables under those prefixes that hold no secret: an
|
|
343
|
+
* identity, a rendering switch, a path. Listed rather than pattern-matched so
|
|
344
|
+
* that adding one is a deliberate act with a reviewer.
|
|
345
|
+
*/
|
|
346
|
+
export declare const NON_SECRET_ENV_NAMES: readonly string[];
|
|
347
|
+
/**
|
|
348
|
+
* Does this bare variable name name credential material?
|
|
349
|
+
*
|
|
350
|
+
* Exported since APRV-205 for the same reason the two lists are: the scrub that
|
|
351
|
+
* builds a granted child's environment asks exactly this question, of a real
|
|
352
|
+
* environment rather than of command text, and it must ask it the same way.
|
|
353
|
+
*/
|
|
354
|
+
export declare function isSecretEnvName(name: string): boolean;
|
|
355
|
+
/**
|
|
356
|
+
* Facts about the machine the command will run on, resolved by the CALLER
|
|
357
|
+
* (APRV-267).
|
|
358
|
+
*
|
|
359
|
+
* The classifier is pure and stays pure. Some rules, though, turn on something
|
|
360
|
+
* no string can carry: whether a path names the agent's own scratch space. So
|
|
361
|
+
* the impure half is hoisted out of this file entirely — the caller resolves
|
|
362
|
+
* the roots, this file only compares path segments against them — and the
|
|
363
|
+
* shape is the one `protectedPaths` already established: an optional argument
|
|
364
|
+
* whose absence yields the strictly NARROWER answer. A caller that forgets it
|
|
365
|
+
* classifies every delete the way this file classified it before the field
|
|
366
|
+
* existed; it never invents an authorization.
|
|
367
|
+
*
|
|
368
|
+
* Every root must be ABSOLUTE and already resolved (symlinks followed) by the
|
|
369
|
+
* caller. This file does not touch the disk and cannot check either property,
|
|
370
|
+
* so a caller handing it a relative or unresolved root gets segment matching
|
|
371
|
+
* against exactly what it passed.
|
|
372
|
+
*/
|
|
373
|
+
export interface ClassifierContext {
|
|
374
|
+
/**
|
|
375
|
+
* Roots under which a delete is the agent tidying after itself: the session
|
|
376
|
+
* scratchpad the harness allots, and the system temp directory.
|
|
377
|
+
*
|
|
378
|
+
* `src/cli/hook.ts` resolves these (`resolveScratchRoots`) and tightens the
|
|
379
|
+
* answer afterwards with the checks that need the disk — a symlink escaping
|
|
380
|
+
* the root, a git checkout living inside it. Nothing here is a grant on its
|
|
381
|
+
* own: a path under a root still has to survive that second pass.
|
|
382
|
+
*/
|
|
383
|
+
scratchRoots?: readonly string[];
|
|
384
|
+
}
|
|
385
|
+
/** Everything a refinement needs: the binary and the words that followed it. */
|
|
386
|
+
interface RuleContext {
|
|
387
|
+
bin: string;
|
|
388
|
+
/** Words after the binary, quotes already removed. */
|
|
389
|
+
args: string[];
|
|
390
|
+
/** Words after the binary that are not flags. */
|
|
391
|
+
positionals: string[];
|
|
392
|
+
/** The matched subcommand (first positional), or `null`. */
|
|
393
|
+
sub: string | null;
|
|
394
|
+
/**
|
|
395
|
+
* Did any of those words come out of a command substitution? Its text is
|
|
396
|
+
* gone by the time a rule sees it, so a rule that reads its arguments closely
|
|
397
|
+
* (APRV-114's fetch refinement) needs to know that one of them is a hole.
|
|
398
|
+
*/
|
|
399
|
+
substituted: boolean;
|
|
400
|
+
/** What the caller knows about the machine (APRV-267). Never read from here. */
|
|
401
|
+
context: ClassifierContext;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* A refinement's answer: a class and the rule id that chose it, or a statement
|
|
405
|
+
* that this invocation cannot be read at all.
|
|
406
|
+
*
|
|
407
|
+
* The opaque arm is APRV-283's. A refinement that returns `null` is answered
|
|
408
|
+
* with the interpreter message (`runs inline source`), which is true of
|
|
409
|
+
* `node -e` and false of `find … -exec`, and a refusal that misdescribes the
|
|
410
|
+
* command it refuses sends the agent looking for a flag it did not pass. A
|
|
411
|
+
* refinement that has its own reason states it.
|
|
412
|
+
*/
|
|
413
|
+
type Refinement = {
|
|
414
|
+
class: string;
|
|
415
|
+
rule: string;
|
|
416
|
+
} | {
|
|
417
|
+
opaque: string;
|
|
418
|
+
};
|
|
419
|
+
/**
|
|
420
|
+
* One row of the classification table.
|
|
421
|
+
*
|
|
422
|
+
* `bins` + `subs` is the match; `class` is the answer. A row with a `refine`
|
|
423
|
+
* looks at the flags before answering, and declares every class it can emit in
|
|
424
|
+
* `emits` so the table stays enumerable (the dogfood test reads that list).
|
|
425
|
+
*/
|
|
426
|
+
export interface CommandRule {
|
|
427
|
+
/** Stable identifier, printed by `hook classify` and pinned by the fixtures. */
|
|
428
|
+
id: string;
|
|
429
|
+
bins: readonly string[];
|
|
430
|
+
/** Match only when the first positional is one of these. */
|
|
431
|
+
subs?: readonly string[];
|
|
432
|
+
class: string;
|
|
433
|
+
/** Additional classes a refinement may return. */
|
|
434
|
+
emits?: readonly string[];
|
|
435
|
+
/** Flag-sensitive answer; falls back to `class` when it returns `null`. */
|
|
436
|
+
refine?: (ctx: RuleContext) => Refinement | null;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* The table.
|
|
440
|
+
*
|
|
441
|
+
* Order matters: the first row whose binary and subcommand match decides. Rows
|
|
442
|
+
* are grouped by binary, strictest interpretation first within a binary, and
|
|
443
|
+
* every class named here is one SPEC.md §7 declares (§7's developer-workstation
|
|
444
|
+
* namespaces, plus `read.shell` / `read.vcs.remote` / `read.web` under
|
|
445
|
+
* `read.*`), with one addition: the `log.*` namespace of the two verbs that
|
|
446
|
+
* move the log file, introduced by SPEC §10.1's APRV-125 amendment.
|
|
447
|
+
*/
|
|
448
|
+
export declare const COMMAND_RULES: readonly CommandRule[];
|
|
449
|
+
/**
|
|
450
|
+
* The rules whose commands RUN CODE THIS RUNTIME DID NOT AUTHOR (APRV-193).
|
|
451
|
+
*
|
|
452
|
+
* Rule ids rather than classes, because the class does not separate them: `npm
|
|
453
|
+
* test`, `node build.mjs`, `tsc` and `mkdir` all resolve to
|
|
454
|
+
* `files.write.workspace`, and only the first three execute a file an agent may
|
|
455
|
+
* have written a minute ago. That is the whole distinction laundering turns on
|
|
456
|
+
* — the command's NAME stops describing its effect exactly when the effect is
|
|
457
|
+
* in a file the name does not mention — so it is drawn here, once, where a
|
|
458
|
+
* future rule's author will see it.
|
|
459
|
+
*
|
|
460
|
+
* Read by `APPROVAL_HOOK_REQUIRE_SANDBOX` (`src/cli/hook.ts`) and by nothing
|
|
461
|
+
* else. It grants nothing and denies nothing on its own: it says which commands
|
|
462
|
+
* the hook may be asked to require a sandbox for, and the requirement is off
|
|
463
|
+
* unless an operator turns it on.
|
|
464
|
+
*/
|
|
465
|
+
export declare const CODE_EXECUTING_RULES: readonly string[];
|
|
466
|
+
/**
|
|
467
|
+
* Every class the table can emit, for docs and for the dogfood test.
|
|
468
|
+
*
|
|
469
|
+
* Fixed, and it does not include the `policy.edit` sub-classes (APRV-266): a
|
|
470
|
+
* routed class is emitted only because a particular policy named it, so the set
|
|
471
|
+
* of them is a property of that file rather than of this table. A reader
|
|
472
|
+
* asking "can the classifier ever emit this class?" of a routed name must ask
|
|
473
|
+
* it WITH the policy in hand — {@link emittableClass} is that question.
|
|
474
|
+
*/
|
|
475
|
+
export declare const CLASSIFIER_CLASSES: readonly string[];
|
|
476
|
+
/**
|
|
477
|
+
* Can the classifier emit `actionClass` for a project whose policy carries
|
|
478
|
+
* these `protected_paths`? (APRV-266.)
|
|
479
|
+
*
|
|
480
|
+
* {@link CLASSIFIER_CLASSES} answers for the binary table, which is fixed. A
|
|
481
|
+
* routed class is not in that table and never will be: it exists because one
|
|
482
|
+
* policy wrote it beside one path, and the same name in another project's
|
|
483
|
+
* policy would be a different class over different files. So the reachability
|
|
484
|
+
* question — the one `core/policy-expectations.ts` asks of every class a policy
|
|
485
|
+
* declares, so that a policy line nobody can ever fire is caught at the
|
|
486
|
+
* ceremony rather than believed for a year — takes the policy's own entries.
|
|
487
|
+
*
|
|
488
|
+
* A routed name is reachable exactly when some entry routes to it. A
|
|
489
|
+
* `policy.edit.spec` rule in a policy whose `protected_paths` routes nothing to
|
|
490
|
+
* it is a line that will never fire, and saying so is the whole point.
|
|
491
|
+
*/
|
|
492
|
+
export declare function emittableClass(actionClass: string, protectedPaths?: readonly ProtectedPathEntry[]): boolean;
|
|
493
|
+
/**
|
|
494
|
+
* Classify a shell command line into the classes it would produce.
|
|
495
|
+
*
|
|
496
|
+
* Every segment must classify: one unreadable segment refuses the whole
|
|
497
|
+
* command, because a command line's effect is the union of its parts and a
|
|
498
|
+
* partial answer would authorize the parts we happened to understand.
|
|
499
|
+
*
|
|
500
|
+
* `protectedPaths` is `policy.protected_paths` (APRV-107), added to the
|
|
501
|
+
* built-in protected set rather than replacing it. Omitting it classifies
|
|
502
|
+
* against the built-ins alone, which is the strictly narrower answer, so a
|
|
503
|
+
* caller that forgets it under-reports the protected classes rather than inventing an
|
|
504
|
+
* authorization; every enforcement path passes the loaded policy's list.
|
|
505
|
+
*
|
|
506
|
+
* Since APRV-266 an entry may be `{path, class}`, routing that path family to a
|
|
507
|
+
* `policy.edit` sub-class. The classifier stays what it was: the entry's class
|
|
508
|
+
* is DATA it copies out of the policy, matched by the same segment matcher as
|
|
509
|
+
* every other entry, so this resolves no autonomy at all.
|
|
510
|
+
*
|
|
511
|
+
* `context` (APRV-267) carries the machine facts a caller has resolved: today
|
|
512
|
+
* only `scratchRoots`. It behaves exactly as `protectedPaths` does: omitting it
|
|
513
|
+
* yields the strictly narrower answer, because every rule that reads it can only
|
|
514
|
+
* ever LOOSEN a class, and no rule reads it to loosen a protected or credential
|
|
515
|
+
* one.
|
|
516
|
+
*/
|
|
517
|
+
export declare function classifyCommand(command: string, protectedPaths?: readonly ProtectedPathEntry[], context?: ClassifierContext): CommandClassification;
|
|
518
|
+
/** One segment's words, as the classifier's own tokenizer read them. */
|
|
519
|
+
export interface CommandSegmentWords {
|
|
520
|
+
/** The segment's source text, as written. */
|
|
521
|
+
text: string;
|
|
522
|
+
/** The binary, `VAR=value` prefixes already skipped, quotes already removed. */
|
|
523
|
+
bin: string;
|
|
524
|
+
/** Every word after the binary, flags included, in order. */
|
|
525
|
+
args: string[];
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* The words of each segment, from the SAME parse {@link classifyCommand} uses.
|
|
529
|
+
*
|
|
530
|
+
* Exported for the channel-side command breakdown (APRV-144): a prompt that
|
|
531
|
+
* says what a compound command does needs the verb and the arguments of each
|
|
532
|
+
* segment, and a display layer that re-split the string itself would be a
|
|
533
|
+
* second tokenizer, free to disagree with the one that chose the class. This
|
|
534
|
+
* runs {@link lex} — the tokenizer — and applies the same assignment-prefix
|
|
535
|
+
* skip `classifySegment` applies, and stops there: it classifies nothing and
|
|
536
|
+
* decides nothing.
|
|
537
|
+
*
|
|
538
|
+
* `null` when the tokenizer refuses the string, which is the same input
|
|
539
|
+
* `classifyCommand` answers `unparseable` for. Segments carrying no binary (a
|
|
540
|
+
* bare assignment, a lone redirection) are omitted: they have no verb to show.
|
|
541
|
+
*/
|
|
542
|
+
export declare function commandSegmentWords(command: string): CommandSegmentWords[] | null;
|
|
543
|
+
export {};
|
|
@@ -240,6 +240,16 @@ export function protectedPathClass(candidate, extra = []) {
|
|
|
240
240
|
if (next === "hooks.json" || next === "hooks" || next === "agents")
|
|
241
241
|
return "policy.core";
|
|
242
242
|
}
|
|
243
|
+
// Codex installs its hook through these configuration and script paths.
|
|
244
|
+
if (segment === ".codex") {
|
|
245
|
+
const next = segments[index + 1];
|
|
246
|
+
if (next === undefined ||
|
|
247
|
+
next === "config.toml" ||
|
|
248
|
+
next === "hooks.json" ||
|
|
249
|
+
next === "hooks" ||
|
|
250
|
+
segments.slice(index + 1).includes(".."))
|
|
251
|
+
return "policy.core";
|
|
252
|
+
}
|
|
243
253
|
}
|
|
244
254
|
// 3. The policy's own routed entries (APRV-266), above the built-in
|
|
245
255
|
// `policy.edit` set so a routing can re-label one of those paths, and
|
|
@@ -868,27 +878,49 @@ function hasShortFlag(args, letters) {
|
|
|
868
878
|
function isUnknownValue(word) {
|
|
869
879
|
return word.includes("$") || word.includes("*") || word.includes("?") || word.startsWith("~");
|
|
870
880
|
}
|
|
871
|
-
/**
|
|
881
|
+
/** A bare release tag: the conventional `v` plus a semantic-version-shaped value. */
|
|
882
|
+
const V_PREFIXED_SEMVER = /^v(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u;
|
|
883
|
+
/** Does one side of a push refspec explicitly name a tag? */
|
|
884
|
+
function isTagRef(name) {
|
|
885
|
+
if (name.startsWith("refs/tags/"))
|
|
886
|
+
return true;
|
|
887
|
+
if (name.startsWith("refs/heads/"))
|
|
888
|
+
return false;
|
|
889
|
+
return V_PREFIXED_SEMVER.test(name);
|
|
890
|
+
}
|
|
891
|
+
/** Does either source or destination of this refspec explicitly name a tag? */
|
|
892
|
+
function isTagRefspec(refspec) {
|
|
893
|
+
const colon = refspec.indexOf(":");
|
|
894
|
+
if (colon === -1)
|
|
895
|
+
return isTagRef(refspec);
|
|
896
|
+
return isTagRef(refspec.slice(0, colon)) || isTagRef(refspec.slice(colon + 1));
|
|
897
|
+
}
|
|
898
|
+
/** `git push` — force, release, trunk and branch classes turn on flags and refspecs. */
|
|
872
899
|
function refineGitPush(ctx) {
|
|
873
900
|
const args = ctx.args.slice(1);
|
|
874
|
-
if (hasFlag(args, ["--force", "-f", "--force-with-lease", "--force-if-includes"])) {
|
|
901
|
+
if (hasFlag(args, ["--force", "-f", "--force-with-lease", "--force-if-includes", "--mirror"])) {
|
|
875
902
|
return { class: "vcs.history.rewrite", rule: "git-push-force" };
|
|
876
903
|
}
|
|
877
904
|
const positionals = args.filter((arg) => !isFlag(arg));
|
|
878
|
-
|
|
879
|
-
|
|
905
|
+
const refspecs = positionals.slice(1);
|
|
906
|
+
if (refspecs.some((refspec) => refspec.startsWith("+"))) {
|
|
907
|
+
return { class: "vcs.history.rewrite", rule: "git-push-force" };
|
|
908
|
+
}
|
|
909
|
+
if (hasFlag(args, ["--tags", "--follow-tags"]) ||
|
|
910
|
+
refspecs.some(isTagRefspec) ||
|
|
911
|
+
refspecs.some((word, index) => word === "tag" && index + 1 < refspecs.length)) {
|
|
912
|
+
return { class: "release.publish", rule: "git-push-tag" };
|
|
913
|
+
}
|
|
914
|
+
// A non-tag deletion, or a push with no refspec at all: the destination is
|
|
915
|
+
// either the trunk or unknown, and unknown resolves to the stricter class.
|
|
880
916
|
if (hasFlag(args, ["--delete", "-d"])) {
|
|
881
917
|
return { class: "vcs.push.main", rule: "git-push-delete" };
|
|
882
918
|
}
|
|
883
|
-
const refspecs = positionals.slice(1);
|
|
884
919
|
if (refspecs.length === 0) {
|
|
885
920
|
return { class: "vcs.push.main", rule: "git-push-implicit" };
|
|
886
921
|
}
|
|
887
922
|
let sawMain = false;
|
|
888
923
|
for (const refspec of refspecs) {
|
|
889
|
-
if (refspec.startsWith("+")) {
|
|
890
|
-
return { class: "vcs.history.rewrite", rule: "git-push-force" };
|
|
891
|
-
}
|
|
892
924
|
const colon = refspec.indexOf(":");
|
|
893
925
|
const destination = colon === -1 ? refspec : refspec.slice(colon + 1);
|
|
894
926
|
// `:branch` (empty source) and `src:` (empty destination) both delete a
|
|
@@ -1401,6 +1433,9 @@ function isGateEntrypoint(path) {
|
|
|
1401
1433
|
function refineApprovalVerb(positionals) {
|
|
1402
1434
|
const verb = positionals[0];
|
|
1403
1435
|
const sub = positionals[1];
|
|
1436
|
+
if (verb === "quickstart") {
|
|
1437
|
+
return { class: "policy.core", rule: "approval-quickstart" };
|
|
1438
|
+
}
|
|
1404
1439
|
if (verb === "log") {
|
|
1405
1440
|
if (sub === "sync")
|
|
1406
1441
|
return { class: "log.sync", rule: "approval-log-sync" };
|