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,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the log says when a human decided and the gate could not take it
|
|
3
|
+
* (APRV-235, amended SPEC.md §5.2).
|
|
4
|
+
*
|
|
5
|
+
* ## The fact this module is about
|
|
6
|
+
*
|
|
7
|
+
* Seen live on 2026-09-02, after the seq 13704 ceremony. Carter tapped approve
|
|
8
|
+
* on a request that had been asked under the previous policy. The gate refused
|
|
9
|
+
* `policy-drift`, which is right: the rules the approver was shown are not the
|
|
10
|
+
* rules in force, so a grant recorded there would claim a decision under a
|
|
11
|
+
* policy nobody put in front of them. But the refusal went to the operator's
|
|
12
|
+
* terminal and nowhere else. Nothing was appended, the request stayed pending
|
|
13
|
+
* in `QUEUE.md` and on Telegram, and the person who had tapped saw no reaction
|
|
14
|
+
* at all.
|
|
15
|
+
*
|
|
16
|
+
* Three things were wrong and this module fixes two of them (the third, the
|
|
17
|
+
* message edit, belongs to the channels).
|
|
18
|
+
*
|
|
19
|
+
* ## 1. A human's decision is a fact even when the gate cannot honour it
|
|
20
|
+
*
|
|
21
|
+
* The log is the truth. SPEC.md §11 makes human attention the audit budget, and
|
|
22
|
+
* an approver's tap SPENDS it whether or not an authorization comes out the
|
|
23
|
+
* other end. A reader looking at that log later found a request sitting
|
|
24
|
+
* unanswered with nothing to say a person had answered it, which is a question
|
|
25
|
+
* the log is supposed to be able to settle.
|
|
26
|
+
*
|
|
27
|
+
* So {@link recordRefusedDecision} appends one `audit.decision_refused`. It is
|
|
28
|
+
* an **audit-tier** record in the strict sense:
|
|
29
|
+
*
|
|
30
|
+
* - it grants nothing and mints no token;
|
|
31
|
+
* - `core/state.ts` does not settle a request on it, so no verdict moves;
|
|
32
|
+
* - `core/budgets.ts` charges nothing for it, so no budget moves;
|
|
33
|
+
* - `core/audit.ts` does not sample it and no streak counts it.
|
|
34
|
+
*
|
|
35
|
+
* `tests/decision-refusal.test.ts` asserts that last group by construction: the
|
|
36
|
+
* derived request state, the budget verdicts and the sampling draw are taken
|
|
37
|
+
* before and after and compared, and they are identical.
|
|
38
|
+
*
|
|
39
|
+
* ## The asymmetry: humans yes, agents no
|
|
40
|
+
*
|
|
41
|
+
* Gate-side refusals handed to an AGENT stay unlogged, and that is a choice
|
|
42
|
+
* rather than an omission. An agent that is refused reads the code, stops or
|
|
43
|
+
* asks again, and nothing was spent that a record could account for. A human
|
|
44
|
+
* who is refused has already spent the scarce thing, and cannot be asked to
|
|
45
|
+
* spend it again without the log saying the first spend happened. Recording
|
|
46
|
+
* every agent-side refusal would also put the busiest path in the runtime — a
|
|
47
|
+
* hook classifying every shell command — into the append path, which is a cost
|
|
48
|
+
* with no reader.
|
|
49
|
+
*
|
|
50
|
+
* ## 2. A request the gate calls void must stop being offered
|
|
51
|
+
*
|
|
52
|
+
* `policy-drift` is the one refusal where the gate does not merely decline this
|
|
53
|
+
* decision, it declares the REQUEST dead: "the pending request is void and the
|
|
54
|
+
* action must be requested again". Leaving it pending afterwards offers every
|
|
55
|
+
* approver on every channel a tap that cannot be honoured, forever, until the
|
|
56
|
+
* TTL lapses. So a drift refusal also appends `approval.withdrawn` with the
|
|
57
|
+
* runtime's own reason, `policy-drift` (APRV-106 gave the event; the schema's
|
|
58
|
+
* cross-rule makes that reason `system:`-only and `system:` that-reason-only).
|
|
59
|
+
* `approval queue`, `QUEUE.md` and every channel then read the request as
|
|
60
|
+
* settled, because they all derive state from `core/state.ts` and it settles on
|
|
61
|
+
* `approval.withdrawn` without looking at who wrote it or why.
|
|
62
|
+
*
|
|
63
|
+
* The action is not stranded: its caller requests it again, and the new request
|
|
64
|
+
* is routed, budgeted and displayed under the policy actually in force. That is
|
|
65
|
+
* what the refusal message already told them to do.
|
|
66
|
+
*
|
|
67
|
+
* ## Order, and what an interrupted write leaves behind
|
|
68
|
+
*
|
|
69
|
+
* The audit record is appended first and the withdrawal second, naming it in
|
|
70
|
+
* `payload.refused_seq`. Two appends are two records — the log never batches —
|
|
71
|
+
* so a crash between them is a state this code can reach, and the order is
|
|
72
|
+
* chosen for which half is safe to have alone. The explanation without the
|
|
73
|
+
* withdrawal leaves the request pending exactly as it is today, and a reader can
|
|
74
|
+
* see why. The withdrawal without the explanation would take a request out of a
|
|
75
|
+
* human's queue with nothing on the record saying who tapped, or why it was
|
|
76
|
+
* refused, which is the failure this task exists to end.
|
|
77
|
+
*
|
|
78
|
+
* ## Invariants
|
|
79
|
+
*
|
|
80
|
+
* - **Gate-typed events never accept caller timestamps** (§11.1). `ts` comes
|
|
81
|
+
* from the injected clock at the write boundary; there is no parameter.
|
|
82
|
+
* - **Every check-then-append passes through compare-and-append** (§11.1(5)).
|
|
83
|
+
* Both writes state the head they were derived against, and the whole cycle
|
|
84
|
+
* re-enters from a fresh read on `head-moved` through
|
|
85
|
+
* {@link withHeadRetry} — the same bounded retry APRV-236 gave `decide`.
|
|
86
|
+
* - **Self-reported fields never reduce scrutiny** (§11.1). Everything here only
|
|
87
|
+
* ADDS to what a reviewer sees. The approver's identity comes from the
|
|
88
|
+
* decision surface's configured actor, the same source a grant's does, and the
|
|
89
|
+
* record's own actor is `system:` so that neither party to the refusal is its
|
|
90
|
+
* author.
|
|
91
|
+
* - **Refusals stay machine-readable and distinct** (§11.1). The gate's code is
|
|
92
|
+
* copied verbatim; nothing here invents, merges or softens one.
|
|
93
|
+
*
|
|
94
|
+
* ## No attestation check, deliberately
|
|
95
|
+
*
|
|
96
|
+
* Nothing here asks whether the policy is attested, for the reason `reject` and
|
|
97
|
+
* `revoke` do not: this write confers no authority. Refusing to record a refusal
|
|
98
|
+
* because a file changed would be the strict direction pointing the wrong way,
|
|
99
|
+
* and the case where it would bite hardest is `policy-not-attested` itself —
|
|
100
|
+
* exactly the refusal an operator most needs the log to remember. The write
|
|
101
|
+
* boundary still validates every record, so what lands is still constrained.
|
|
102
|
+
*/
|
|
103
|
+
import { type AppendError, type EventRecord } from "./log.js";
|
|
104
|
+
import { type Decision } from "./state.js";
|
|
105
|
+
import type { GateOptions } from "./gate.js";
|
|
106
|
+
/**
|
|
107
|
+
* The actor every record here carries. `system:`, and the same id the runtime's
|
|
108
|
+
* other unprompted writes use: this is the gate stating what the gate did.
|
|
109
|
+
*/
|
|
110
|
+
export declare const DECISION_REFUSAL_ACTOR = "system:gate";
|
|
111
|
+
/** The runtime's withdrawal reason, closed to requesters by the event schema. */
|
|
112
|
+
export declare const POLICY_DRIFT_REASON = "policy-drift";
|
|
113
|
+
/** Who decided what, on which surface — everything the record needs about the tap. */
|
|
114
|
+
export interface RefusedDecision {
|
|
115
|
+
/** The action whose decision was refused. */
|
|
116
|
+
actionKey: string;
|
|
117
|
+
/** Which of the three human-only verbs was attempted. */
|
|
118
|
+
decision: Decision;
|
|
119
|
+
/** The approver, `human:<id>`, from the surface's configured identity. */
|
|
120
|
+
actor: string;
|
|
121
|
+
/** The surface that collected the gesture: `telegram`, `web`, `cli`. */
|
|
122
|
+
channel: string;
|
|
123
|
+
}
|
|
124
|
+
/** The gate refusal being recorded. Only these fields are ever read. */
|
|
125
|
+
export interface RefusalFacts {
|
|
126
|
+
/** The gate's code, verbatim. */
|
|
127
|
+
code: string;
|
|
128
|
+
/** The gate's message, verbatim. */
|
|
129
|
+
message: string;
|
|
130
|
+
/**
|
|
131
|
+
* The two policy hashes the gate compared, on `policy-drift` alone. Supplied
|
|
132
|
+
* by the refusal that made the comparison rather than re-derived here: a
|
|
133
|
+
* second derivation could disagree with the one that actually refused, and
|
|
134
|
+
* then the record would describe a comparison nobody made.
|
|
135
|
+
*/
|
|
136
|
+
drift?: {
|
|
137
|
+
requested: string;
|
|
138
|
+
attested: string;
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/** What the module could not do. Never thrown; the caller carries on regardless. */
|
|
142
|
+
export interface RefusalRecordFailure {
|
|
143
|
+
ok: false;
|
|
144
|
+
code: "log-unreadable" | "append-failed";
|
|
145
|
+
message: string;
|
|
146
|
+
append?: AppendError;
|
|
147
|
+
}
|
|
148
|
+
export type RecordRefusedDecisionResult = {
|
|
149
|
+
ok: true;
|
|
150
|
+
/**
|
|
151
|
+
* The `audit.decision_refused` record, or `null` when the refused actor
|
|
152
|
+
* was not a person and there was therefore nothing to record.
|
|
153
|
+
*/
|
|
154
|
+
audit: EventRecord | null;
|
|
155
|
+
/** The `approval.withdrawn` record, on `policy-drift` alone; `null` otherwise. */
|
|
156
|
+
withdrawn: EventRecord | null;
|
|
157
|
+
} | RefusalRecordFailure;
|
|
158
|
+
/** Is this the refusal that declares the request itself void? */
|
|
159
|
+
export declare function voidsTheRequest(code: string): boolean;
|
|
160
|
+
/**
|
|
161
|
+
* Record that a human's decision was refused, and withdraw the request when the
|
|
162
|
+
* refusal was the gate declaring it void.
|
|
163
|
+
*
|
|
164
|
+
* Called by the decision SURFACES — `channels/contract.ts`'s
|
|
165
|
+
* `recordChannelDecision` and `cli/gate.ts`'s `commandDecide` — and never from
|
|
166
|
+
* inside `decide()`. That placement is deliberate twice over. `decide()`'s
|
|
167
|
+
* contract is that a refusal appends nothing, which is what lets a caller retry
|
|
168
|
+
* one without wondering what it wrote; and this record is about a HUMAN having
|
|
169
|
+
* decided, which is a fact only a surface that collected a human's gesture can
|
|
170
|
+
* assert.
|
|
171
|
+
*
|
|
172
|
+
* Best-effort by design: a failure here is returned, never thrown, and the
|
|
173
|
+
* caller shows the gate's refusal either way. The decision was already refused
|
|
174
|
+
* before this ran, and nothing about that outcome depends on this write landing.
|
|
175
|
+
*
|
|
176
|
+
* **Two writes, two retry cycles, and deliberately not one.** `head-retry.ts`
|
|
177
|
+
* asks that the unit of retry be a whole read-check-append cycle, and putting
|
|
178
|
+
* both appends inside one cycle would break that in the direction that matters:
|
|
179
|
+
* a moved head under the SECOND write would re-enter from the top and append a
|
|
180
|
+
* second `audit.decision_refused` for one tap. So each write has its own cycle,
|
|
181
|
+
* each re-reads and re-derives, and the withdrawal's cycle re-checks that the
|
|
182
|
+
* request is still pending — a decision or a withdrawal that landed in the
|
|
183
|
+
* window is the new verdict, and there is then nothing left to withdraw.
|
|
184
|
+
*/
|
|
185
|
+
export declare function recordRefusedDecision(logPath: string, decided: RefusedDecision, refusal: RefusalFacts, options?: GateOptions): RecordRefusedDecisionResult;
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `.approval/env` — the environment SOURCE MAP (SPEC.md §5.2, §11; APRV-73).
|
|
3
|
+
*
|
|
4
|
+
* Every secret this runtime touches is named by the policy and held in an
|
|
5
|
+
* environment variable: `channels.telegram.token_env` (§5.1), the chat id
|
|
6
|
+
* beside it, `audit.sampling_secret_env` (§5.2), `vault.passphrase_env` (§5.2),
|
|
7
|
+
* and `APPROVAL_HUMAN`, which is human identity itself (§11). The policy carries
|
|
8
|
+
* NAMES and never values, which is the right boundary and leaves an operator
|
|
9
|
+
* with five variables to get into a shell before any gate operation works, and
|
|
10
|
+
* no written-down place to say where they come from.
|
|
11
|
+
*
|
|
12
|
+
* This module is that written-down place. `.approval/env` is a source map, not
|
|
13
|
+
* a secret store: `KEY=VALUE` lines whose VALUE says WHERE the value lives.
|
|
14
|
+
*
|
|
15
|
+
* ```
|
|
16
|
+
* # one line per variable; # comments and blank lines are ignored
|
|
17
|
+
* APPROVAL_HUMAN=human:alice
|
|
18
|
+
* APPROVAL_TG_TOKEN=keychain:approval-telegram-token
|
|
19
|
+
* APPROVAL_VAULT_PASSPHRASE=secret-service:vault-passphrase
|
|
20
|
+
* APPROVAL_AUDIT_SECRET=env:
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* Four value forms, and one of them is deliberately unpleasant:
|
|
24
|
+
*
|
|
25
|
+
* - `keychain:<service>` — macOS, `security find-generic-password -a "$USER"
|
|
26
|
+
* -s <service> -w`. The value comes back on stdout and is never in an argv.
|
|
27
|
+
* - `secret-service:<label>` — Linux desktop, `secret-tool lookup approval
|
|
28
|
+
* <label>`. Same property.
|
|
29
|
+
* - `env:` — inherited from the ambient environment. A documentation form: it
|
|
30
|
+
* asserts "this one comes from the shell that launched you", and resolves to
|
|
31
|
+
* nothing on its own.
|
|
32
|
+
* - a bare literal — permitted, and ALWAYS reported as plaintext. A rule people
|
|
33
|
+
* route around is not a control: an operator who is told plainly that their
|
|
34
|
+
* token is sitting in a file in the working tree can decide to accept that,
|
|
35
|
+
* and an operator who is forbidden from writing it there writes it into their
|
|
36
|
+
* shell profile instead, where nothing in this runtime can see it to say so.
|
|
37
|
+
*
|
|
38
|
+
* A value carrying some OTHER `word:` prefix is a literal, not an error, because
|
|
39
|
+
* `APPROVAL_HUMAN=human:alice` is the commonest line this file will ever hold.
|
|
40
|
+
* Near misses of the real schemes are reserved by name and refused rather than
|
|
41
|
+
* silently exported as text; see {@link RESERVED_SCHEMES}.
|
|
42
|
+
*
|
|
43
|
+
* ## THE LOAD-BEARING RULE: nothing loads this file implicitly
|
|
44
|
+
*
|
|
45
|
+
* No verb in this runtime reads `.approval/env` into its own environment. One
|
|
46
|
+
* verb, `approval env`, resolves it and prints an export block for a shell to
|
|
47
|
+
* evaluate; every other command sees only the environment the human who
|
|
48
|
+
* launched it had already established.
|
|
49
|
+
*
|
|
50
|
+
* The reason is `APPROVAL_HUMAN`. SPEC.md §11 states that human identity in v0.1
|
|
51
|
+
* is config-declared and that "the trust boundary is the local machine, and
|
|
52
|
+
* anyone who can set that configuration and write to the log is inside it". A
|
|
53
|
+
* working-tree file that could set `APPROVAL_HUMAN` would move that boundary to
|
|
54
|
+
* "anyone who can write a file in the repository": an agent that can edit source
|
|
55
|
+
* — which every agent working in a repository can — would thereby be able to
|
|
56
|
+
* attest a policy, grant its own request, and set a vault credential, on every
|
|
57
|
+
* invocation, as the human. The whole human-only gate would be one `echo >>`
|
|
58
|
+
* away from being nobody's gate.
|
|
59
|
+
*
|
|
60
|
+
* So the file is inert. A human evaluates `approval env` in their own shell,
|
|
61
|
+
* sees the export block that is about to run (or checks it value-free first with
|
|
62
|
+
* `--check`), and the process that performs a gate operation inherits an
|
|
63
|
+
* environment a human established. This is SPEC.md §11.1 invariant 7, and
|
|
64
|
+
* `tests/cli-env.test.ts` pins it by spawning `doctor`, `policy attest` and
|
|
65
|
+
* `channel telegram health` in a directory holding a complete `.approval/env`
|
|
66
|
+
* and asserting that none of them saw a byte of it.
|
|
67
|
+
*
|
|
68
|
+
* ## Mode 0600
|
|
69
|
+
*
|
|
70
|
+
* A file that may hold a literal secret is refused unless its mode is exactly
|
|
71
|
+
* `0600`, and the refusal prints the `chmod`. This is a lock on a door whose
|
|
72
|
+
* wall is missing (the same session can chmod it back), and it is worth having
|
|
73
|
+
* for the reason `umask` is worth having: the common failure is a
|
|
74
|
+
* world-readable file nobody looked at, not an adversary in the room.
|
|
75
|
+
*
|
|
76
|
+
* ## Determinism, and the one place it stops
|
|
77
|
+
*
|
|
78
|
+
* Parsing is a pure function of the bytes. Resolution is not: it shells out to
|
|
79
|
+
* helper binaries, which is why {@link SourceRunner} exists as an injectable
|
|
80
|
+
* seam and why the tests drive stub `security` / `secret-tool` scripts through
|
|
81
|
+
* PATH rather than touching a real Keychain.
|
|
82
|
+
*
|
|
83
|
+
* Nothing here throws. Every failure is a `{ ok: false, code, message }` from
|
|
84
|
+
* the frozen union {@link ENV_FILE_REFUSAL_CODES}. No credential VALUE appears
|
|
85
|
+
* in a refusal, a message, or a `source` label on any path.
|
|
86
|
+
*/
|
|
87
|
+
import type { PolicyLoadResult } from "./policy-load.js";
|
|
88
|
+
/** The source map's filename, beside the log's home: `.approval/env`. */
|
|
89
|
+
export declare const ENV_FILENAME = "env";
|
|
90
|
+
/**
|
|
91
|
+
* The env file for a given log path — derived exactly as `vaultPathFor` derives
|
|
92
|
+
* the vault, so the log, the payload store, the vault and this file stay under
|
|
93
|
+
* one home: SPEC.md §9 fixes the log at `<home>/log/events.jsonl`, so the source
|
|
94
|
+
* map is `<home>/env`, a sibling of the log DIRECTORY and never inside it.
|
|
95
|
+
*/
|
|
96
|
+
export declare function envFilePathFor(logPath: string): string;
|
|
97
|
+
/** The mode the file must have. Anything else is refused. */
|
|
98
|
+
export declare const ENV_FILE_REQUIRED_MODE = 384;
|
|
99
|
+
/**
|
|
100
|
+
* Everything this module can refuse. Frozen public API, per SPEC.md §11.1(6).
|
|
101
|
+
*
|
|
102
|
+
* Each code names a different repair, which is the test of whether a code earns
|
|
103
|
+
* its place. The three helper codes are separate for exactly that reason: "you
|
|
104
|
+
* are on a machine without `secret-tool`", "the item is not in your keychain",
|
|
105
|
+
* and "the helper ran and failed" are three different mornings.
|
|
106
|
+
*/
|
|
107
|
+
export declare const ENV_FILE_REFUSAL_CODES: readonly [
|
|
108
|
+
/** The file's mode is not 0600. The refusal carries the `chmod` to run. */
|
|
109
|
+
"env-file-mode",
|
|
110
|
+
/**
|
|
111
|
+
* The file exists and could not be read, stat'd, or (APRV-74) written. A
|
|
112
|
+
* filesystem fact in every case, with a filesystem repair, which is why the
|
|
113
|
+
* write path reuses this code rather than adding a fourth I/O name to a
|
|
114
|
+
* frozen union: "the directory is read-only" and "the file is unreadable"
|
|
115
|
+
* are the same morning and the same exit code.
|
|
116
|
+
*/
|
|
117
|
+
"env-file-io",
|
|
118
|
+
/** A line is neither blank, nor a comment, nor `KEY=VALUE`. */
|
|
119
|
+
"env-file-syntax",
|
|
120
|
+
/** A KEY does not match `[A-Z_][A-Z0-9_]*`. No `export ` prefix is accepted. */
|
|
121
|
+
"env-file-key-invalid",
|
|
122
|
+
/** The same KEY appears twice. Which one wins is not a thing to guess at. */
|
|
123
|
+
"env-file-duplicate-key",
|
|
124
|
+
/** A VALUE carries a `scheme:` prefix this build does not implement. */
|
|
125
|
+
"env-file-unknown-scheme",
|
|
126
|
+
/** A KEY with an empty VALUE. An empty secret is a configuration error. */
|
|
127
|
+
"env-file-empty-value",
|
|
128
|
+
/** The helper binary for a scheme is not on PATH. Not the operator's fault. */
|
|
129
|
+
"helper-binary-missing",
|
|
130
|
+
/** The helper ran and the named item is not there. Store it, or fix the name. */
|
|
131
|
+
"helper-item-missing",
|
|
132
|
+
/** The helper ran and failed for some other reason (locked keyring, …). */
|
|
133
|
+
"helper-failed",
|
|
134
|
+
/**
|
|
135
|
+
* A policy declared an `_env` NAME that is not a usable shell variable name.
|
|
136
|
+
* Never emitted as an `export` line: the export block is evaluated by a shell,
|
|
137
|
+
* and a name carrying a space or a `;` would be a policy file executing code.
|
|
138
|
+
*/
|
|
139
|
+
"invalid-variable-name"];
|
|
140
|
+
export type EnvFileRefusalCode = (typeof ENV_FILE_REFUSAL_CODES)[number];
|
|
141
|
+
/** A whole-file failure. Nothing here throws. */
|
|
142
|
+
export interface EnvFileRefusal {
|
|
143
|
+
ok: false;
|
|
144
|
+
code: EnvFileRefusalCode;
|
|
145
|
+
message: string;
|
|
146
|
+
/** The file the refusal is about. */
|
|
147
|
+
path: string;
|
|
148
|
+
/** 1-based line number, for the parse refusals that have one. */
|
|
149
|
+
line?: number;
|
|
150
|
+
}
|
|
151
|
+
/** The four value forms. `literal` is the fallback and the explicit escape. */
|
|
152
|
+
export type EnvSourceKind = "keychain" | "secret-service" | "env" | "literal";
|
|
153
|
+
/** One `KEY=VALUE` line, parsed. */
|
|
154
|
+
export interface EnvFileEntry {
|
|
155
|
+
key: string;
|
|
156
|
+
kind: EnvSourceKind;
|
|
157
|
+
/**
|
|
158
|
+
* The service name, the label, or the literal value. Empty for `env:`.
|
|
159
|
+
*
|
|
160
|
+
* For `literal` this IS the secret, so it is never put in a message, a
|
|
161
|
+
* `source` label, or a refusal by anything in this module.
|
|
162
|
+
*/
|
|
163
|
+
argument: string;
|
|
164
|
+
/** 1-based line number, so a diagnostic can point at the line. */
|
|
165
|
+
line: number;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Parse the file's text. No interpolation, no quote stripping, no `export `
|
|
169
|
+
* prefix, no line continuations: this is a source map, not a shell script, and
|
|
170
|
+
* every one of those features is a way for a file to mean something other than
|
|
171
|
+
* what it looks like.
|
|
172
|
+
*
|
|
173
|
+
* Quotes are NOT stripped, which is worth saying out loud because `.env` files
|
|
174
|
+
* elsewhere do strip them: here `A="b"` is the five-character literal `"b"`.
|
|
175
|
+
*/
|
|
176
|
+
export declare function parseEnvFile(text: string, path: string): {
|
|
177
|
+
ok: true;
|
|
178
|
+
entries: EnvFileEntry[];
|
|
179
|
+
} | EnvFileRefusal;
|
|
180
|
+
/**
|
|
181
|
+
* The digest of an env file's bytes, as read.
|
|
182
|
+
*
|
|
183
|
+
* Not a secret and not derived from one in the sense that matters: the file may
|
|
184
|
+
* carry a plaintext literal, so this is a hash and never the text, and it is
|
|
185
|
+
* one-way. Its only consumer is `core/instance.ts`, which uses it to ask "was
|
|
186
|
+
* this exported value produced from the file as it now reads?" — a question
|
|
187
|
+
* about VERSIONS of a file, which needs an identifier for a version and nothing
|
|
188
|
+
* else (APRV-278).
|
|
189
|
+
*/
|
|
190
|
+
export declare function envFileDigest(text: string): string;
|
|
191
|
+
/** An absent file's digest: the digest of the nothing that was read. */
|
|
192
|
+
export declare const ABSENT_ENV_FILE_DIGEST: string;
|
|
193
|
+
/** The file's contents, or the fact that there is no file. */
|
|
194
|
+
export type EnvFileRead = {
|
|
195
|
+
ok: true;
|
|
196
|
+
present: false;
|
|
197
|
+
path: string;
|
|
198
|
+
entries: [];
|
|
199
|
+
digest: string;
|
|
200
|
+
} | {
|
|
201
|
+
ok: true;
|
|
202
|
+
present: true;
|
|
203
|
+
path: string;
|
|
204
|
+
entries: EnvFileEntry[];
|
|
205
|
+
digest: string;
|
|
206
|
+
} | EnvFileRefusal;
|
|
207
|
+
/**
|
|
208
|
+
* Read and parse the source map.
|
|
209
|
+
*
|
|
210
|
+
* **An absent file is not an error.** Nobody has written one, which is the state
|
|
211
|
+
* of every working directory that keeps its variables in a shell profile, and it
|
|
212
|
+
* is the state `approval init` leaves behind. Every variable then falls to
|
|
213
|
+
* "inherited from the environment" or "unset", which is exactly the world
|
|
214
|
+
* before this file existed.
|
|
215
|
+
*/
|
|
216
|
+
export declare function readEnvFile(path: string): EnvFileRead;
|
|
217
|
+
/** What one upserted KEY did to the file. */
|
|
218
|
+
export interface EnvFileChange {
|
|
219
|
+
key: string;
|
|
220
|
+
/** The line as it will now read: `KEY=VALUE`. Never a secret — see below. */
|
|
221
|
+
value: string;
|
|
222
|
+
/**
|
|
223
|
+
* The VALUE that was on the line before, or `null` when the key was added.
|
|
224
|
+
*
|
|
225
|
+
* This CAN be a plaintext secret, if the operator had written one as a bare
|
|
226
|
+
* literal, so it exists for a caller that needs to decide whether it is
|
|
227
|
+
* REPLACING something, and no caller in this repository prints it. The
|
|
228
|
+
* boolean below is what the CLI reports on.
|
|
229
|
+
*/
|
|
230
|
+
previous: string | null;
|
|
231
|
+
/** The value is unchanged: the line already said exactly this. */
|
|
232
|
+
unchanged: boolean;
|
|
233
|
+
}
|
|
234
|
+
/** A successful write. */
|
|
235
|
+
export interface EnvFileWrite {
|
|
236
|
+
ok: true;
|
|
237
|
+
path: string;
|
|
238
|
+
/** The file did not exist and was created at 0600. */
|
|
239
|
+
created: boolean;
|
|
240
|
+
changes: EnvFileChange[];
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Add or replace `KEY=VALUE` lines, preserving everything else in the file.
|
|
244
|
+
*
|
|
245
|
+
* **Line-oriented, not a rewrite.** The file is read as text, the line whose
|
|
246
|
+
* KEY matches is replaced IN PLACE, and a key that is not present is appended
|
|
247
|
+
* at the end. Comments, blank lines, ordering, and every entry this call was
|
|
248
|
+
* not asked about survive byte for byte. A writer that reparsed and re-emitted
|
|
249
|
+
* would be simpler and would quietly delete the operator's own comments the
|
|
250
|
+
* first time `approval setup channel telegram` ran — this file is one a human edits by
|
|
251
|
+
* hand, and round-trip fidelity for a hand-edited file is the same requirement
|
|
252
|
+
* the Backlog.md task files carry.
|
|
253
|
+
*
|
|
254
|
+
* The file is validated before it is touched: {@link readEnvFile}'s mode check
|
|
255
|
+
* and full parse both run, so `setup` never appends a line to a file it could
|
|
256
|
+
* not have read, and never lands a valid line in a file whose earlier line is a
|
|
257
|
+
* syntax error. A file that does not exist is created at 0600, along with its
|
|
258
|
+
* directory.
|
|
259
|
+
*
|
|
260
|
+
* Callers pass values, and a value here is a SOURCE (`keychain:<service>`), a
|
|
261
|
+
* chat id, or an identity — never a credential, except on the one path where an
|
|
262
|
+
* operator explicitly chose a plaintext literal after being told what it means.
|
|
263
|
+
* Nothing in this function prints anything.
|
|
264
|
+
*/
|
|
265
|
+
export declare function upsertEnvFileEntries(path: string, entries: ReadonlyArray<{
|
|
266
|
+
key: string;
|
|
267
|
+
value: string;
|
|
268
|
+
}>): EnvFileWrite | EnvFileRefusal;
|
|
269
|
+
/** What a helper lookup produced. The value, or a distinct reason it did not. */
|
|
270
|
+
export type SourceOutcome = {
|
|
271
|
+
ok: true;
|
|
272
|
+
value: string;
|
|
273
|
+
} | {
|
|
274
|
+
ok: false;
|
|
275
|
+
code: "helper-binary-missing" | "helper-item-missing" | "helper-failed";
|
|
276
|
+
message: string;
|
|
277
|
+
};
|
|
278
|
+
/**
|
|
279
|
+
* The two helper lookups, injectable.
|
|
280
|
+
*
|
|
281
|
+
* A seam rather than a direct `spawnSync` because the alternative is a test
|
|
282
|
+
* suite that reads the machine's real Keychain, and there is no version of that
|
|
283
|
+
* which is acceptable: it would prompt, it would depend on the developer's own
|
|
284
|
+
* secrets, and on the wrong day it would print one. Tests pass a fake here, and
|
|
285
|
+
* the ONE test that exercises {@link defaultSourceRunner} does it by putting
|
|
286
|
+
* stub `security` / `secret-tool` scripts on the child process's PATH — a real
|
|
287
|
+
* PATH lookup of a real command name, with no test-only flag in the runtime.
|
|
288
|
+
*/
|
|
289
|
+
export interface SourceRunner {
|
|
290
|
+
keychain(service: string): SourceOutcome;
|
|
291
|
+
secretService(label: string): SourceOutcome;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* `security find-generic-password -a "$USER" -s <service> -w` and
|
|
295
|
+
* `secret-tool lookup approval <label>`.
|
|
296
|
+
*
|
|
297
|
+
* THE VALUE IS NEVER IN AN ARGV, in either direction: the argv carries a service
|
|
298
|
+
* name or a label, and the secret comes back on stdout. An argv is world-readable
|
|
299
|
+
* in `ps` for the length of the call, which is the whole reason `approval vault
|
|
300
|
+
* set` has no `--value` flag either.
|
|
301
|
+
*
|
|
302
|
+
* The exit-status readings are documented heuristics, not contracts. `security`
|
|
303
|
+
* exits 44 (`errSecItemNotFound`) for a missing item; `secret-tool` exits 0 with
|
|
304
|
+
* empty output when the lookup matches nothing. Anything else is
|
|
305
|
+
* {@link "helper-failed"}, which is the honest answer for a locked keyring or a
|
|
306
|
+
* D-Bus that is not running: the repair is not "store the item".
|
|
307
|
+
*/
|
|
308
|
+
/**
|
|
309
|
+
* The prefix a deferred lookup carries. See {@link NON_RESOLVING_RUNNER}.
|
|
310
|
+
*/
|
|
311
|
+
export declare const KEYSTORE_DEFERRED = "not resolved by doctor";
|
|
312
|
+
/**
|
|
313
|
+
* A {@link SourceRunner} that looks nothing up (moved here by APRV-178).
|
|
314
|
+
*
|
|
315
|
+
* `security find-generic-password -w` can raise a keychain-unlock or ACL dialog
|
|
316
|
+
* and `secret-tool lookup` can block on a keyring prompt. Either would hang a
|
|
317
|
+
* command run over ssh or from CI, and a command that pops a keychain prompt
|
|
318
|
+
* also TEACHES people to click through keychain prompts. So the diagnostics —
|
|
319
|
+
* `approval doctor`, and `approval up`'s cross-instance report — resolve
|
|
320
|
+
* keystore-backed variables not at all: they report the scheme and the service
|
|
321
|
+
* name, which `.approval/env` already carries in the open, and leave the actual
|
|
322
|
+
* lookup to `approval env --check`, which a human runs deliberately and watches.
|
|
323
|
+
*
|
|
324
|
+
* It lives beside {@link defaultSourceRunner} rather than in one of its callers
|
|
325
|
+
* because two of them now need it and a second copy would be a second set of
|
|
326
|
+
* words for the same refusal.
|
|
327
|
+
*/
|
|
328
|
+
export declare const NON_RESOLVING_RUNNER: SourceRunner;
|
|
329
|
+
export declare const defaultSourceRunner: SourceRunner;
|
|
330
|
+
/** Where a resolved value came from, as a closed set. */
|
|
331
|
+
export type EnvVariableStatus = "set-in-environment" | "resolved-from-keychain" | "resolved-from-secret-service" | "resolved-literal" | "unset";
|
|
332
|
+
/** One variable, resolved. */
|
|
333
|
+
export interface ResolvedVariable {
|
|
334
|
+
/** The variable's NAME, as the policy or the runtime default gave it. */
|
|
335
|
+
name: string;
|
|
336
|
+
status: EnvVariableStatus;
|
|
337
|
+
/** The value, present only when the status is not `unset`. */
|
|
338
|
+
value?: string;
|
|
339
|
+
/**
|
|
340
|
+
* Where it came from, in words, and NEVER a value: `the environment`,
|
|
341
|
+
* `keychain:approval-tg-token`, `literal (plaintext in .approval/env)`,
|
|
342
|
+
* `unset`.
|
|
343
|
+
*/
|
|
344
|
+
source: string;
|
|
345
|
+
/**
|
|
346
|
+
* The value is sitting in plaintext in the working tree: a secret-bearing
|
|
347
|
+
* variable resolved from a bare literal.
|
|
348
|
+
*
|
|
349
|
+
* `APPROVAL_HUMAN` and the Telegram chat id are literals in most real files
|
|
350
|
+
* and are not secrets, so they are `false` — but note the `source` label says
|
|
351
|
+
* "literal (plaintext in .approval/env)" for EVERY literal regardless, because
|
|
352
|
+
* the reader deciding whether a file may be committed wants to see all of
|
|
353
|
+
* them.
|
|
354
|
+
*/
|
|
355
|
+
plaintext: boolean;
|
|
356
|
+
/** What to do about an `unset`. Absent when there is nothing to do. */
|
|
357
|
+
fix?: string;
|
|
358
|
+
/** Why a resolution failed, when one did. Distinct and machine-readable. */
|
|
359
|
+
refusal?: {
|
|
360
|
+
code: EnvFileRefusalCode;
|
|
361
|
+
message: string;
|
|
362
|
+
};
|
|
363
|
+
/**
|
|
364
|
+
* The policy NAMED this variable (rather than the runtime defaulting it).
|
|
365
|
+
* `approval env --check` fails on an unresolved declared variable and not on
|
|
366
|
+
* an unresolved defaulted one: a policy that asked for something is a promise;
|
|
367
|
+
* a default the operator never mentioned is an offer.
|
|
368
|
+
*/
|
|
369
|
+
declared: boolean;
|
|
370
|
+
/** The value would be a secret if it had one: token, passphrase, sampling. */
|
|
371
|
+
secretBearing: boolean;
|
|
372
|
+
/**
|
|
373
|
+
* What the INSTANCE's own `.approval/env` says about this variable, whether
|
|
374
|
+
* or not that is where the value came from (APRV-178).
|
|
375
|
+
*
|
|
376
|
+
* The ambient environment wins over the file and the file is then not even
|
|
377
|
+
* consulted, which is correct and is invariant 7 — but it is also how a
|
|
378
|
+
* production bot token exported in a shell profile silently became a demo
|
|
379
|
+
* instance's channel credential. Nothing could report that, because the
|
|
380
|
+
* resolution discarded the file entry it had ignored. It is kept here so
|
|
381
|
+
* `approval env --check`, `approval doctor` and `approval up` can say "this
|
|
382
|
+
* value is not the one your instance's file names".
|
|
383
|
+
*
|
|
384
|
+
* Value-free by construction: {@link DeclaredSource.service} is filled only
|
|
385
|
+
* for the two keystore schemes, whose argument is a service name the file
|
|
386
|
+
* carries in the open. A `literal` entry's argument IS the secret, so only
|
|
387
|
+
* its KIND is recorded.
|
|
388
|
+
*/
|
|
389
|
+
fileSource?: DeclaredSource;
|
|
390
|
+
}
|
|
391
|
+
/** A `.approval/env` line, reduced to what may be printed. */
|
|
392
|
+
export interface DeclaredSource {
|
|
393
|
+
kind: EnvSourceKind;
|
|
394
|
+
/** The service name or label, for `keychain:` and `secret-service:` only. */
|
|
395
|
+
service?: string;
|
|
396
|
+
/** 1-based line number in `.approval/env`. */
|
|
397
|
+
line: number;
|
|
398
|
+
}
|
|
399
|
+
/** A declared source in words, and never a value. */
|
|
400
|
+
export declare function describeDeclaredSource(source: DeclaredSource): string;
|
|
401
|
+
/** The whole answer. */
|
|
402
|
+
export interface EnvResolution {
|
|
403
|
+
ok: true;
|
|
404
|
+
/** Was there a file at all? */
|
|
405
|
+
present: boolean;
|
|
406
|
+
path: string;
|
|
407
|
+
/**
|
|
408
|
+
* {@link envFileDigest} of the bytes this resolution read (APRV-278), or
|
|
409
|
+
* {@link ABSENT_ENV_FILE_DIGEST} when there was no file. It identifies a
|
|
410
|
+
* VERSION of the file and carries none of its contents.
|
|
411
|
+
*/
|
|
412
|
+
digest: string;
|
|
413
|
+
variables: ResolvedVariable[];
|
|
414
|
+
}
|
|
415
|
+
interface Wanted {
|
|
416
|
+
name: string;
|
|
417
|
+
declared: boolean;
|
|
418
|
+
secretBearing: boolean;
|
|
419
|
+
/** The repair for an unset one. */
|
|
420
|
+
fix: string;
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* The variables `approval env` answers for, in a stable order.
|
|
424
|
+
*
|
|
425
|
+
* Five by name — human identity, the Telegram token and chat id, the vault
|
|
426
|
+
* passphrase, and the sampling secret — plus whatever else the policy names by
|
|
427
|
+
* the `_env` convention.
|
|
428
|
+
*
|
|
429
|
+
* The sampling secret is the one conditional member: `audit.sampling_secret_env`
|
|
430
|
+
* has NO default (an unnamed one disables sampling, SPEC.md §5.2), so listing a
|
|
431
|
+
* made-up variable for it would invent configuration the operator never chose.
|
|
432
|
+
* The other four all have defaults and are always listed.
|
|
433
|
+
*/
|
|
434
|
+
export declare function wantedVariables(load: PolicyLoadResult): Wanted[];
|
|
435
|
+
/**
|
|
436
|
+
* Resolve every variable the policy implies, against the ambient environment
|
|
437
|
+
* first and the source map second.
|
|
438
|
+
*
|
|
439
|
+
* **The ambient environment always wins.** A variable already exported in the
|
|
440
|
+
* calling shell is reported `set-in-environment` and its file entry is not even
|
|
441
|
+
* consulted: the human's shell is the authority (that is invariant 7's whole
|
|
442
|
+
* point), and a file that could override an exported value would be a file that
|
|
443
|
+
* silently redirects a gate operation's credentials.
|
|
444
|
+
*
|
|
445
|
+
* A whole-file refusal (bad mode, unreadable, unparseable) is returned as-is:
|
|
446
|
+
* partial resolution of a file the runtime cannot fully read is how a typo turns
|
|
447
|
+
* into a half-configured environment.
|
|
448
|
+
*/
|
|
449
|
+
export declare function resolveEnvironment(load: PolicyLoadResult, envFilePath: string, runner?: SourceRunner, ambientEnv?: NodeJS.ProcessEnv): EnvResolution | EnvFileRefusal;
|
|
450
|
+
export {};
|