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,500 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human-signed log checkpoints (APRV-220).
|
|
3
|
+
*
|
|
4
|
+
* ## The hole this fills
|
|
5
|
+
*
|
|
6
|
+
* The chain in `.approval/log/events.jsonl` is unkeyed, and
|
|
7
|
+
* `docs/proposals/incremental-prefix-proof.md` §3 states the consequence: a
|
|
8
|
+
* process with write access to that file can truncate it and recompute a chain
|
|
9
|
+
* that is self-consistent from genesis. Nothing INSIDE the file contradicts the
|
|
10
|
+
* forgery, so no walk of it ever will. The conformance suite says the same from
|
|
11
|
+
* the other side — `chain-verification/truncation-unanchored` is a boundary
|
|
12
|
+
* vector, and an implementation that claims to catch an unanchored truncation
|
|
13
|
+
* is claiming more than a hash chain can give.
|
|
14
|
+
*
|
|
15
|
+
* §12 of that proposal named three ways out: external anchoring, a keyed chain,
|
|
16
|
+
* and human-signed checkpoints. APRV-219 built the first. This module builds
|
|
17
|
+
* the third.
|
|
18
|
+
*
|
|
19
|
+
* ## How the two witnesses relate
|
|
20
|
+
*
|
|
21
|
+
* They are independent, and neither weakens the other.
|
|
22
|
+
*
|
|
23
|
+
* The ANCHOR (`cli/log-anchor.ts`) asks "does somebody else hold a copy of
|
|
24
|
+
* these bytes?" and answers from git. It is exactly as fresh as the last push,
|
|
25
|
+
* and on a machine with no remote and no records branch it says nothing at all
|
|
26
|
+
* (a skip, never a pass).
|
|
27
|
+
*
|
|
28
|
+
* A CHECKPOINT asks "did a key that no agent process holds sign this head?" and
|
|
29
|
+
* answers from the log itself plus the policy. It works offline, it covers the
|
|
30
|
+
* window since the last push, and it survives being copied to another machine.
|
|
31
|
+
*
|
|
32
|
+
* Against the forger of §3 they fail in different directions, which is the
|
|
33
|
+
* point of having both: the anchor catches a truncation whose records somebody
|
|
34
|
+
* else already holds, and a checkpoint catches a truncation inside the window
|
|
35
|
+
* nobody has pushed yet — because every checkpoint in the rewritten range names
|
|
36
|
+
* a `(seq, hash)` the rewritten chain does not carry, and the forger cannot
|
|
37
|
+
* produce a signature over the hashes they DID recompute.
|
|
38
|
+
*
|
|
39
|
+
* `approval log verify` runs the two independently and reports both. Neither
|
|
40
|
+
* check may be weakened to make the other pass.
|
|
41
|
+
*
|
|
42
|
+
* ## The key
|
|
43
|
+
*
|
|
44
|
+
* A DEDICATED Ed25519 keypair, not the attestation identity. Attestation
|
|
45
|
+
* (`core/attest.ts`) has no keypair to reuse: human identity at v0.1 is
|
|
46
|
+
* config-declared (`--as human:<id>`), and its whole documented claim is that
|
|
47
|
+
* *someone with local control* signed off, not who. A checkpoint has to claim
|
|
48
|
+
* more than that or it claims nothing, because the party it defends against is
|
|
49
|
+
* a process with local control.
|
|
50
|
+
*
|
|
51
|
+
* - The PRIVATE half never appears in the log, in a policy, or in any file an
|
|
52
|
+
* agent may read: it lives in the credential vault (`core/vault.ts`),
|
|
53
|
+
* encrypted at rest under a passphrase `core/child-env.ts` strips from every
|
|
54
|
+
* spawned child, behind a file whose reading classifies `account.credential`
|
|
55
|
+
* (human-only). This module never reads it from anywhere — the caller passes
|
|
56
|
+
* the bytes, so the custody decision lives in one place, the CLI verb.
|
|
57
|
+
* - The PUBLIC half is listed in the policy, `audit.checkpoint_keys`. The
|
|
58
|
+
* policy is the human's own committed, attested artifact: editing it is a
|
|
59
|
+
* visible diff AND de-attests the policy, so gate operations refuse until a
|
|
60
|
+
* human re-attests. That is a materially harder thing to do quietly than
|
|
61
|
+
* rewriting a log line.
|
|
62
|
+
*
|
|
63
|
+
* A list rather than a scalar, so rotation can RETAIN a retired key. A
|
|
64
|
+
* checkpoint signed by a key the policy no longer lists is a refusal here, on
|
|
65
|
+
* purpose (see {@link CHECKPOINT_REFUSAL_CODES}), which makes dropping a key
|
|
66
|
+
* that signed anything a de-verification rather than a cleanup.
|
|
67
|
+
*
|
|
68
|
+
* ## What is signed, and why not more
|
|
69
|
+
*
|
|
70
|
+
* `"approval.md/log-checkpoint/v1\n" + JCS({alg, hash, seq})`. The prefix is
|
|
71
|
+
* domain separation: a signature made here cannot be lifted into any other use
|
|
72
|
+
* of the same key, and a signature made elsewhere cannot be presented as a
|
|
73
|
+
* checkpoint. The head's `hash` is a 256-bit chain digest, so the message is
|
|
74
|
+
* already specific to one chain at one position and needs no further binding.
|
|
75
|
+
*
|
|
76
|
+
* The signature deliberately does NOT cover the rest of the record. It could
|
|
77
|
+
* not: the record's own `hash` covers its payload, which covers the signature.
|
|
78
|
+
* What a checkpoint asserts is exactly "a key holder saw this head" — every
|
|
79
|
+
* other field of the record is covered by the chain, and by the anchor, and by
|
|
80
|
+
* this module refusing a checkpoint whose signed head is not the head the log
|
|
81
|
+
* actually carries.
|
|
82
|
+
*
|
|
83
|
+
* ## Fail closed on a bad signature, fail open on an absent one
|
|
84
|
+
*
|
|
85
|
+
* An invalid signature, an unknown key, or a signed hash the log contradicts is
|
|
86
|
+
* a refusal. A log with no checkpoints at all is not: a human who has been away
|
|
87
|
+
* is not a forger, and a runtime that refused a log for want of a tap would
|
|
88
|
+
* teach its operator to turn the check off. A configured cadence that has
|
|
89
|
+
* lapsed is a WARNING, at every layer, and there is no path in this module from
|
|
90
|
+
* "due" to "refused".
|
|
91
|
+
*
|
|
92
|
+
* A missing PUBLIC KEY is a skip naming why, never a pass — the same rule the
|
|
93
|
+
* anchor check follows for a missing anchor. Nothing has been verified, and
|
|
94
|
+
* reporting silence as a pass is how a check stops being one.
|
|
95
|
+
*/
|
|
96
|
+
import { type ClockOptions } from "./clock.js";
|
|
97
|
+
import { type AppendError, type AppendOptions, type EventRecord, type LogHead } from "./log.js";
|
|
98
|
+
/** The one signature scheme at v0.1. Recorded on every checkpoint. */
|
|
99
|
+
export declare const CHECKPOINT_ALG = "ed25519";
|
|
100
|
+
/** The event type a checkpoint is written as. */
|
|
101
|
+
export declare const CHECKPOINT_EVENT = "log.checkpoint";
|
|
102
|
+
/**
|
|
103
|
+
* Domain separation for the signed message. A constant, and a versioned one:
|
|
104
|
+
* if the signed shape ever changes, the prefix changes with it, so a signature
|
|
105
|
+
* over the old shape can never be read as one over the new.
|
|
106
|
+
*/
|
|
107
|
+
export declare const CHECKPOINT_DOMAIN = "approval.md/log-checkpoint/v1";
|
|
108
|
+
/** The credential name the CLI reads the private half from. */
|
|
109
|
+
export declare const CHECKPOINT_KEY_CREDENTIAL = "approval.checkpoint.key";
|
|
110
|
+
/**
|
|
111
|
+
* Every way a checkpoint can refuse a range. A closed union per SPEC.md §11.1
|
|
112
|
+
* invariant 6, frozen the way the others are: callers branch on the string.
|
|
113
|
+
*
|
|
114
|
+
* Five codes, because they are five different facts about how a checkpoint
|
|
115
|
+
* failed and they have five different repairs. Collapsing them would leave the
|
|
116
|
+
* one message a person needs — which record, which key, which seq — inside a
|
|
117
|
+
* free-text blob nothing can branch on.
|
|
118
|
+
*
|
|
119
|
+
* `checkpoint-key-unknown` is a refusal rather than a warning, and that is the
|
|
120
|
+
* load-bearing choice in this union. Softening it would hand a forger the
|
|
121
|
+
* escape hatch: rewrite each checkpoint's `key_sha256` to name a key nobody
|
|
122
|
+
* lists, and every refusal in the range becomes a shrug. The cost is that
|
|
123
|
+
* removing a retired key from `audit.checkpoint_keys` stops the checkpoints it
|
|
124
|
+
* signed from verifying, which is why the policy field is a LIST and why
|
|
125
|
+
* APRV-257's rotation verb refuses to drop a key that signed anything.
|
|
126
|
+
*/
|
|
127
|
+
export declare const CHECKPOINT_REFUSAL_CODES: readonly [
|
|
128
|
+
/**
|
|
129
|
+
* The record names a `key_sha256` that no configured public key hashes to.
|
|
130
|
+
* Either a key was retired out of the policy, or somebody wrote a checkpoint
|
|
131
|
+
* with a key of their own — and this runtime cannot tell which, so it refuses.
|
|
132
|
+
*/
|
|
133
|
+
"checkpoint-key-unknown",
|
|
134
|
+
/**
|
|
135
|
+
* The signature does not verify under the key the record names. The bytes
|
|
136
|
+
* signed are not the bytes presented; nothing further is inferred, because
|
|
137
|
+
* distinguishing "wrong key" from "tampered payload" would be an oracle and
|
|
138
|
+
* the repair is identical.
|
|
139
|
+
*/
|
|
140
|
+
"checkpoint-signature-invalid",
|
|
141
|
+
/**
|
|
142
|
+
* The signature is good and the log disagrees with it: the record at the
|
|
143
|
+
* signed `seq` carries a hash the signature does not name (or the log carries
|
|
144
|
+
* no record at that seq at all). THIS is the forged-chain catch — a chain
|
|
145
|
+
* recomputed after a checkpoint cannot reproduce a signature over the hashes
|
|
146
|
+
* it replaced.
|
|
147
|
+
*/
|
|
148
|
+
"checkpoint-hash-mismatch",
|
|
149
|
+
/**
|
|
150
|
+
* The signed `seq` is not below the checkpoint record's own. A checkpoint
|
|
151
|
+
* signs the past; one naming itself or the future is not a checkpoint, and a
|
|
152
|
+
* runtime that accepted one would accept a record vouching for a head that
|
|
153
|
+
* did not exist when it was written.
|
|
154
|
+
*/
|
|
155
|
+
"checkpoint-out-of-order",
|
|
156
|
+
/**
|
|
157
|
+
* The payload is not a checkpoint payload: a missing field, a hash that is
|
|
158
|
+
* not 64 hex, an `alg` this build does not implement. The write boundary
|
|
159
|
+
* refuses these, so reaching one means the record came from elsewhere — and a
|
|
160
|
+
* `log.checkpoint` nobody can read is not a checkpoint that passes.
|
|
161
|
+
*/
|
|
162
|
+
"checkpoint-malformed"];
|
|
163
|
+
export type CheckpointRefusalCode = (typeof CHECKPOINT_REFUSAL_CODES)[number];
|
|
164
|
+
/** A checkpoint keypair. The public half travels; the private half never does. */
|
|
165
|
+
export interface CheckpointKeypair {
|
|
166
|
+
/** Base64 DER SPKI. What goes in `audit.checkpoint_keys`. */
|
|
167
|
+
publicKey: string;
|
|
168
|
+
/** Base64 DER PKCS#8. What goes in the vault, and nowhere else. */
|
|
169
|
+
privateKey: string;
|
|
170
|
+
/** SHA-256 of the DER SPKI bytes, hex. What the record names. */
|
|
171
|
+
fingerprint: string;
|
|
172
|
+
}
|
|
173
|
+
/** Mint a checkpoint keypair. Ed25519 from `node:crypto`; no dependency added. */
|
|
174
|
+
export declare function mintCheckpointKeypair(): CheckpointKeypair;
|
|
175
|
+
/**
|
|
176
|
+
* SHA-256 of a public key's DER SPKI **bytes**, hex.
|
|
177
|
+
*
|
|
178
|
+
* Over the bytes rather than over their base64 spelling, so a key that was
|
|
179
|
+
* re-wrapped, re-encoded, or copied through a text editor still fingerprints to
|
|
180
|
+
* the same value. Returns `null` for anything that is not a public key this
|
|
181
|
+
* build can parse — an unreadable key is not a key, and callers turn that into
|
|
182
|
+
* a skip or a refusal rather than into a match against nothing.
|
|
183
|
+
*/
|
|
184
|
+
export declare function checkpointKeyFingerprint(publicKey: string): string | null;
|
|
185
|
+
/**
|
|
186
|
+
* The configured public keys, indexed by fingerprint.
|
|
187
|
+
*
|
|
188
|
+
* Keys that do not parse are DROPPED rather than throwing, and the count of
|
|
189
|
+
* what survived is what a caller reports: a policy listing one good key and one
|
|
190
|
+
* typo must still verify the checkpoints the good key signed, and it must not
|
|
191
|
+
* be able to claim the typo verified anything.
|
|
192
|
+
*/
|
|
193
|
+
export declare function checkpointKeyIndex(publicKeys: readonly string[]): {
|
|
194
|
+
keys: Map<string, string>;
|
|
195
|
+
unreadable: number;
|
|
196
|
+
};
|
|
197
|
+
/** The head a checkpoint signs, exactly as the record records it. */
|
|
198
|
+
export interface CheckpointHead {
|
|
199
|
+
seq: number;
|
|
200
|
+
hash: string;
|
|
201
|
+
alg?: string;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* The bytes a checkpoint signature covers.
|
|
205
|
+
*
|
|
206
|
+
* JCS (RFC 8785) over `{alg, hash, seq}`, behind {@link CHECKPOINT_DOMAIN} and
|
|
207
|
+
* a newline. Canonical, so two runtimes that agree on the head agree on the
|
|
208
|
+
* bytes; domain-separated, so the signature means one thing.
|
|
209
|
+
*/
|
|
210
|
+
export declare function checkpointMessage(head: CheckpointHead): Buffer;
|
|
211
|
+
/** Sign a head with a base64 PKCS#8 private key. `null` when the key is unusable. */
|
|
212
|
+
export declare function signCheckpoint(head: CheckpointHead, privateKey: string): string | null;
|
|
213
|
+
/** Does `signature` verify over `head` under `publicKey`? Never throws. */
|
|
214
|
+
export declare function verifyCheckpointSignature(head: CheckpointHead, signature: string, publicKey: string): boolean;
|
|
215
|
+
/** A `log.checkpoint` payload, as this runtime reads one back. */
|
|
216
|
+
export interface CheckpointPayload {
|
|
217
|
+
seq: number;
|
|
218
|
+
hash: string;
|
|
219
|
+
alg: string;
|
|
220
|
+
keySha256: string;
|
|
221
|
+
signature: string;
|
|
222
|
+
}
|
|
223
|
+
/** Is this record a `log.checkpoint`? Type only; the payload is read separately. */
|
|
224
|
+
export declare function isCheckpointRecord(record: EventRecord): boolean;
|
|
225
|
+
/**
|
|
226
|
+
* Read a checkpoint record's payload, or `null` when it is not one.
|
|
227
|
+
*
|
|
228
|
+
* Strict, and deliberately so: the write boundary already refuses a malformed
|
|
229
|
+
* checkpoint, so a payload that fails here arrived from somewhere else, and the
|
|
230
|
+
* caller's answer to that is {@link CHECKPOINT_REFUSAL_CODES}'s
|
|
231
|
+
* `checkpoint-malformed` rather than a pass.
|
|
232
|
+
*/
|
|
233
|
+
export declare function readCheckpointPayload(record: EventRecord): CheckpointPayload | null;
|
|
234
|
+
/**
|
|
235
|
+
* Which keys have signed checkpoints in this log, and at which seqs (APRV-257).
|
|
236
|
+
*
|
|
237
|
+
* The question rotation has to ask before it retires anything. Removing a key
|
|
238
|
+
* from `audit.checkpoint_keys` turns every checkpoint it signed into
|
|
239
|
+
* `checkpoint-key-unknown` — a REFUSAL, by the deliberate choice recorded in
|
|
240
|
+
* {@link CHECKPOINT_REFUSAL_CODES} — so a verb that let an operator drop a key
|
|
241
|
+
* casually would be a verb that broke a log's verification to tidy a list.
|
|
242
|
+
*
|
|
243
|
+
* Keyed by the record's self-reported `key_sha256`, which is the only thing
|
|
244
|
+
* `checkLogCheckpoints` looks a key up by, so the answer is exactly the set of
|
|
245
|
+
* names whose removal would change a verdict. Records with an unreadable
|
|
246
|
+
* payload are skipped: they refuse for a different reason already, and no
|
|
247
|
+
* removal makes that better or worse.
|
|
248
|
+
*/
|
|
249
|
+
export declare function checkpointSignersIn(records: readonly EventRecord[]): Map<string, number[]>;
|
|
250
|
+
/** Why a checkpoint was not appended. Nothing was written in any of these. */
|
|
251
|
+
export declare const CHECKPOINT_APPEND_REFUSAL_CODES: readonly [
|
|
252
|
+
/** The actor is not `human:`-prefixed. Refused here and in the schema. */
|
|
253
|
+
"actor-not-human",
|
|
254
|
+
/** The private key will not parse, or would not sign. */
|
|
255
|
+
"checkpoint-key-unusable",
|
|
256
|
+
/** The log has no records yet: an empty chain has no head to sign. */
|
|
257
|
+
"log-empty",
|
|
258
|
+
/**
|
|
259
|
+
* A caller named a head this log does not carry (APRV-257).
|
|
260
|
+
*
|
|
261
|
+
* Only {@link appendCheckpointAt} can reach it, and only from the tap: the
|
|
262
|
+
* human is shown a `(seq, hash)`, the head moves while the phone is in a
|
|
263
|
+
* pocket, and the record they sign still names the head they SAW. That is
|
|
264
|
+
* allowed — a checkpoint signs any seq below its own — but only while the
|
|
265
|
+
* log actually carries that hash at that seq. When it does not, the thing in
|
|
266
|
+
* front of the human was derived from a different chain than the one being
|
|
267
|
+
* written to, and signing it would mint a checkpoint that
|
|
268
|
+
* `checkpoint-hash-mismatch` refuses forever after.
|
|
269
|
+
*/
|
|
270
|
+
"checkpoint-head-unknown",
|
|
271
|
+
/** The log could not be opened. */
|
|
272
|
+
"log-unreadable",
|
|
273
|
+
/** The log's last line is truncated. Nothing may chain onto it. */
|
|
274
|
+
"log-torn-tail",
|
|
275
|
+
/** The chain does not verify, so no head derived from it may be signed. */
|
|
276
|
+
"log-corrupt",
|
|
277
|
+
/** The append itself was refused; `append` carries the writer's own code. */
|
|
278
|
+
"append-failed"];
|
|
279
|
+
export type CheckpointAppendRefusalCode = (typeof CHECKPOINT_APPEND_REFUSAL_CODES)[number];
|
|
280
|
+
export interface CheckpointAppendRefusal {
|
|
281
|
+
ok: false;
|
|
282
|
+
code: CheckpointAppendRefusalCode;
|
|
283
|
+
message: string;
|
|
284
|
+
/** The writer's own refusal, present when `code` is `append-failed`. */
|
|
285
|
+
append?: AppendError;
|
|
286
|
+
}
|
|
287
|
+
export interface CheckpointAppendResult {
|
|
288
|
+
ok: true;
|
|
289
|
+
record: EventRecord;
|
|
290
|
+
/** The head this checkpoint signed. */
|
|
291
|
+
head: LogHead;
|
|
292
|
+
/** The fingerprint of the key that signed it. */
|
|
293
|
+
fingerprint: string;
|
|
294
|
+
}
|
|
295
|
+
export interface CheckpointAppendOptions extends ClockOptions {
|
|
296
|
+
schemaDir?: string;
|
|
297
|
+
append?: AppendOptions;
|
|
298
|
+
/**
|
|
299
|
+
* The channel the record names (APRV-257). `cli` by default, which is what
|
|
300
|
+
* the terminal verb is; the tap passes the channel the human tapped on.
|
|
301
|
+
*
|
|
302
|
+
* Descriptive and never authoritative: the schema constrains the ACTOR of a
|
|
303
|
+
* `log.checkpoint` and the signature constrains the head, and neither of
|
|
304
|
+
* those reads this field. It is here so a reader of the log can tell a
|
|
305
|
+
* checkpoint taken at a terminal from one taken on a phone.
|
|
306
|
+
*/
|
|
307
|
+
channel?: string;
|
|
308
|
+
/**
|
|
309
|
+
* How many whole read-sign-append cycles a moved head may cost (APRV-257).
|
|
310
|
+
*
|
|
311
|
+
* The tap needs it and the terminal verb inherits it: a listener signing on a
|
|
312
|
+
* busy log races the daemon's own appends, and handing `head-moved` back to
|
|
313
|
+
* someone who has just tapped a button on their phone is precisely the party
|
|
314
|
+
* `core/head-retry.ts` exists to stop handing it to. Each attempt re-reads
|
|
315
|
+
* and re-signs; nothing crosses an attempt.
|
|
316
|
+
*/
|
|
317
|
+
attempts?: number;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Append one `log.checkpoint` signing the log's current head.
|
|
321
|
+
*
|
|
322
|
+
* The head is read, then signed, then written with that head as
|
|
323
|
+
* `expectedHead`, so the record cannot land on a chain that moved underneath it
|
|
324
|
+
* (SPEC.md §11.1: every check-then-append passes through compare-and-append).
|
|
325
|
+
* A concurrent append is `head-moved`, and the repair is to run it again — the
|
|
326
|
+
* signature would otherwise vouch for a head that is no longer this record's
|
|
327
|
+
* predecessor, which is a checkpoint that verifies and means less than it looks
|
|
328
|
+
* like it means.
|
|
329
|
+
*
|
|
330
|
+
* `ts` is stamped from the clock at the write boundary and is never a
|
|
331
|
+
* parameter: `log.checkpoint` is gate-typed under amended SPEC.md §8 (A2), and
|
|
332
|
+
* a caller who could choose the moment could backdate the one record whose
|
|
333
|
+
* whole content is a claim about a moment.
|
|
334
|
+
*
|
|
335
|
+
* The private key arrives as a value. This module never reads it from a vault,
|
|
336
|
+
* a file, or an environment variable, so there is exactly one place in the
|
|
337
|
+
* codebase that decides where a checkpoint key may come from, and it is the CLI
|
|
338
|
+
* verb a human runs.
|
|
339
|
+
*/
|
|
340
|
+
export declare function appendCheckpoint(logPath: string, privateKey: string, actor: string, options?: CheckpointAppendOptions): CheckpointAppendResult | CheckpointAppendRefusal;
|
|
341
|
+
/**
|
|
342
|
+
* Append one `log.checkpoint` signing a head the CALLER names (APRV-257).
|
|
343
|
+
*
|
|
344
|
+
* The tap's entry point, and the reason APRV-220's verify rule asks only that a
|
|
345
|
+
* checkpoint signs a seq BELOW its own rather than its immediate predecessor.
|
|
346
|
+
* A human is shown `(seq, hash)` on a phone; by the time they tap, the daemon
|
|
347
|
+
* has appended three records. The honest thing to sign is the head they SAW,
|
|
348
|
+
* because that is what they looked at, and a runtime that quietly re-read the
|
|
349
|
+
* head and signed something else would be putting a human's key over bytes
|
|
350
|
+
* nobody inspected.
|
|
351
|
+
*
|
|
352
|
+
* The named head is checked against the log before anything is signed: it must
|
|
353
|
+
* be a `(seq, hash)` this chain actually carries ({@link
|
|
354
|
+
* CHECKPOINT_APPEND_REFUSAL_CODES}'s `checkpoint-head-unknown`). So a stale
|
|
355
|
+
* prompt from a chain that has since been rewritten cannot be turned into a
|
|
356
|
+
* signature, and the checkpoint that lands is one `checkLogCheckpoints` will
|
|
357
|
+
* accept rather than one it will refuse forever.
|
|
358
|
+
*/
|
|
359
|
+
export declare function appendCheckpointAt(logPath: string, privateKey: string, actor: string, head: CheckpointHead, options?: CheckpointAppendOptions): CheckpointAppendResult | CheckpointAppendRefusal;
|
|
360
|
+
/** The fingerprint of the public half of a private key, or `null`. */
|
|
361
|
+
export declare function privateKeyFingerprint(privateKey: string): string | null;
|
|
362
|
+
/** One checkpoint that validated, as a caller reports it. */
|
|
363
|
+
export interface VerifiedCheckpoint {
|
|
364
|
+
/** The seq of the `log.checkpoint` record itself. */
|
|
365
|
+
at: number;
|
|
366
|
+
/** The head it signed. */
|
|
367
|
+
seq: number;
|
|
368
|
+
hash: string;
|
|
369
|
+
/** When it was signed, from the record's runtime-stamped `ts`. */
|
|
370
|
+
ts: string;
|
|
371
|
+
actor: string;
|
|
372
|
+
keySha256: string;
|
|
373
|
+
}
|
|
374
|
+
/** How the walked range stands against the checkpoints inside it. */
|
|
375
|
+
export type CheckpointCheck = {
|
|
376
|
+
status: "pass";
|
|
377
|
+
/** Every checkpoint that validated, in log order. */
|
|
378
|
+
checkpoints: VerifiedCheckpoint[];
|
|
379
|
+
/** Checkpoints whose signed seq falls below the walked range. */
|
|
380
|
+
unchecked: number;
|
|
381
|
+
/** Configured keys this build could parse. */
|
|
382
|
+
keys: number;
|
|
383
|
+
/** The cadence warning, when one is due. Never a refusal. */
|
|
384
|
+
warning: string | null;
|
|
385
|
+
detail: string;
|
|
386
|
+
} | {
|
|
387
|
+
status: "skip";
|
|
388
|
+
reason: string;
|
|
389
|
+
checkpoints: 0;
|
|
390
|
+
} | {
|
|
391
|
+
status: "refused";
|
|
392
|
+
code: CheckpointRefusalCode;
|
|
393
|
+
/** The seq of the offending `log.checkpoint` record. */
|
|
394
|
+
at: number;
|
|
395
|
+
message: string;
|
|
396
|
+
/** Checkpoints that validated before this one. */
|
|
397
|
+
checkpoints: VerifiedCheckpoint[];
|
|
398
|
+
};
|
|
399
|
+
/** What {@link checkLogCheckpoints} is asked. `records` are already VERIFIED. */
|
|
400
|
+
export interface CheckpointCheckOptions {
|
|
401
|
+
/**
|
|
402
|
+
* The log's records, already verified by the caller.
|
|
403
|
+
*
|
|
404
|
+
* Required rather than re-derived, for SPEC.md §11.1 invariant 1: this check
|
|
405
|
+
* reads only verified records, and a check that walked the chain itself would
|
|
406
|
+
* be answering a question its caller has already answered, differently.
|
|
407
|
+
*/
|
|
408
|
+
records: readonly EventRecord[];
|
|
409
|
+
/** The configured public keys, base64 DER SPKI. From `audit.checkpoint_keys`. */
|
|
410
|
+
publicKeys: readonly string[];
|
|
411
|
+
/**
|
|
412
|
+
* Why there are no keys, when the caller already knows: an unloadable policy,
|
|
413
|
+
* a missing file. Folded into the skip reason so the sentence names the cause
|
|
414
|
+
* rather than only the symptom.
|
|
415
|
+
*/
|
|
416
|
+
keysUnavailable?: string | null;
|
|
417
|
+
/** `audit.checkpoint_every` in milliseconds, or `null` when the cadence is off. */
|
|
418
|
+
checkpointEveryMs?: number | null;
|
|
419
|
+
/** Now, for the cadence warning only. No verdict reads it. */
|
|
420
|
+
now?: number;
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Demand every checkpoint inside the walked range.
|
|
424
|
+
*
|
|
425
|
+
* Every `log.checkpoint` record in `records` must carry a readable payload,
|
|
426
|
+
* name a configured key, verify under it, and name the hash the log actually
|
|
427
|
+
* carries at the seq it signed. The FIRST failure refuses, carrying the seq of
|
|
428
|
+
* the offending record and the checkpoints that validated ahead of it: a person
|
|
429
|
+
* reading a divergence needs to know how far the log was still good.
|
|
430
|
+
*
|
|
431
|
+
* Three things are deliberately not refusals.
|
|
432
|
+
*
|
|
433
|
+
* - **No configured key** is a skip naming why, and naming how many checkpoint
|
|
434
|
+
* records went unchecked. Nothing was verified, and a check that reported
|
|
435
|
+
* that as a pass would have stopped being a check.
|
|
436
|
+
* - **A signed seq below the walked range** is counted and named, not refused.
|
|
437
|
+
* A full walk starts at genesis so this cannot arise there; a caller walking
|
|
438
|
+
* a suffix gets an honest count of what its range could not speak to.
|
|
439
|
+
* - **A lapsed cadence** is a warning. A human who has been away is not a
|
|
440
|
+
* forger, and a runtime that refused a log for want of a tap is a runtime
|
|
441
|
+
* whose operator turns the check off.
|
|
442
|
+
*/
|
|
443
|
+
export declare function checkLogCheckpoints(options: CheckpointCheckOptions): CheckpointCheck;
|
|
444
|
+
/**
|
|
445
|
+
* A checkpoint the runtime would like a human to sign, and the head to show
|
|
446
|
+
* them (APRV-257).
|
|
447
|
+
*
|
|
448
|
+
* `head` is the log's CURRENT head at the moment the offer was made, and it is
|
|
449
|
+
* carried through the prompt into {@link appendCheckpointAt} unchanged. What
|
|
450
|
+
* the human is shown is what gets signed, however long the phone stays in the
|
|
451
|
+
* pocket.
|
|
452
|
+
*/
|
|
453
|
+
export interface CheckpointOffer {
|
|
454
|
+
/** The head a prompt asks the human to sign. */
|
|
455
|
+
head: {
|
|
456
|
+
seq: number;
|
|
457
|
+
hash: string;
|
|
458
|
+
};
|
|
459
|
+
/** The seq of the newest checkpoint RECORD, or `null` when there is none. */
|
|
460
|
+
since: number | null;
|
|
461
|
+
/** How long since that checkpoint (or since the log's oldest record), in ms. */
|
|
462
|
+
ageMs: number;
|
|
463
|
+
/** `audit.checkpoint_every`, in ms. */
|
|
464
|
+
everyMs: number;
|
|
465
|
+
/** The sentence every reporting surface prints. Never a refusal. */
|
|
466
|
+
warning: string;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Is a checkpoint due, and over which head? `null` when it is not.
|
|
470
|
+
*
|
|
471
|
+
* The whole question in one call, over already-verified records: it runs
|
|
472
|
+
* {@link checkLogCheckpoints} and offers only from a PASS. A refused range is
|
|
473
|
+
* not a range to ask for another signature over — the thing to do with a
|
|
474
|
+
* checkpoint that does not verify is look at it, not sign a new one on top —
|
|
475
|
+
* and a skipped one has no key configured, so there is nothing to sign with and
|
|
476
|
+
* nobody to ask.
|
|
477
|
+
*/
|
|
478
|
+
export declare function checkpointDue(options: CheckpointCheckOptions): CheckpointOffer | null;
|
|
479
|
+
/** What the policy says about checkpoints, and why it says nothing when it does. */
|
|
480
|
+
export interface CheckpointPolicy {
|
|
481
|
+
/** `audit.checkpoint_keys`, or empty. */
|
|
482
|
+
publicKeys: string[];
|
|
483
|
+
/** `audit.checkpoint_every` in milliseconds, or `null`. */
|
|
484
|
+
checkpointEveryMs: number | null;
|
|
485
|
+
/** Present when the policy could not be loaded at all. */
|
|
486
|
+
unloadable: string | null;
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* The checkpoint half of a policy, read the way `skewToleranceMsOf` reads its
|
|
490
|
+
* key — except that this one fails to a SKIP rather than to a default.
|
|
491
|
+
*
|
|
492
|
+
* A policy that cannot be loaded configures no keys, and a caller with no keys
|
|
493
|
+
* skips with a reason. There is no safe default here: falling back to "no keys"
|
|
494
|
+
* and calling it a pass would report an unreadable policy as a verified log,
|
|
495
|
+
* and falling back to a built-in key would be a key nobody chose.
|
|
496
|
+
*/
|
|
497
|
+
export declare function checkpointPolicyOf(policy: {
|
|
498
|
+
dir?: string;
|
|
499
|
+
file?: string;
|
|
500
|
+
}, schemaDir?: string): CheckpointPolicy;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The environment a spawned child receives (APRV-205).
|
|
3
|
+
*
|
|
4
|
+
* `approval run` used to call `spawnSync` with no `env` option, so Node handed
|
|
5
|
+
* the child a copy of the whole session environment: the Telegram bot token,
|
|
6
|
+
* whatever `vault.passphrase_env` names, every other credential the session was
|
|
7
|
+
* launched with. APRV-194 closed the direct route by classifying the shell
|
|
8
|
+
* commands that READ credential material, and it could not close this one,
|
|
9
|
+
* because the classifier reads `npm test` and the reading happens inside
|
|
10
|
+
* whatever `npm test` runs. A gate that holds the token while handing it to
|
|
11
|
+
* every child it launches is custody theatre.
|
|
12
|
+
*
|
|
13
|
+
* This module is the scrub. It is the minimal, pre-launch slice of APRV-193's
|
|
14
|
+
* "spawn starved": it removes credential-bearing variables and does nothing
|
|
15
|
+
* else. It is not a sandbox — the child keeps the network, the filesystem, and
|
|
16
|
+
* every other ambient capability of the session, and APRV-193 is where those
|
|
17
|
+
* are taken away.
|
|
18
|
+
*
|
|
19
|
+
* Three rules, in this order:
|
|
20
|
+
*
|
|
21
|
+
* 1. A name the granted action's adapter declared in `requiredCredentials`
|
|
22
|
+
* (APRV-169) is PASSED. That declaration is static, made by the adapter's own
|
|
23
|
+
* code, and reaches this function through nothing a caller typed: a flag that
|
|
24
|
+
* could name a variable to keep would be a flag that hands an agent the
|
|
25
|
+
* token back.
|
|
26
|
+
* 2. A name under the credential-bearing prefixes (`APPROVAL_`, `TELEGRAM_`,
|
|
27
|
+
* `VAULT_`), less the APRV-194 allowlist of runtime names that hold no
|
|
28
|
+
* secret, is REMOVED. The list is imported from `command-class.ts` rather
|
|
29
|
+
* than restated, so the classifier and the scrub cannot drift apart.
|
|
30
|
+
* 3. The name the policy's `vault.passphrase_env` gives is REMOVED, whatever it
|
|
31
|
+
* is. The default (`APPROVAL_VAULT_PASSPHRASE`) is already caught by rule 2;
|
|
32
|
+
* a deployment that renamed it to something outside the prefixes is the
|
|
33
|
+
* reason this rule is separate.
|
|
34
|
+
*
|
|
35
|
+
* Everything else passes through untouched. `PATH`, `HOME`, `TMPDIR`, `LANG`,
|
|
36
|
+
* `NODE_OPTIONS` and the rest of a working environment are not this task's
|
|
37
|
+
* business, and a scrub that broke `PATH` would be reverted within the day.
|
|
38
|
+
* That is an allowlist inverted, and the design says so plainly: APRV-193's
|
|
39
|
+
* §3.4 wants a real allowlist at the SESSION boundary, where the operator
|
|
40
|
+
* launches the harness; a per-child allowlist here would break every command an
|
|
41
|
+
* agent legitimately runs.
|
|
42
|
+
*
|
|
43
|
+
* What comes back beside the environment is a COUNT. The log records how many
|
|
44
|
+
* variables were withheld and never which ones, because a name is the half of a
|
|
45
|
+
* credential this repository can print, and SPEC.md §11.1's raw-secrets
|
|
46
|
+
* invariant is not satisfied by leaking the other half slowly. The count is
|
|
47
|
+
* informational: nothing in the gate reads it back, and no decision anywhere
|
|
48
|
+
* turns on it.
|
|
49
|
+
*/
|
|
50
|
+
import { NON_SECRET_ENV_NAMES, SECRET_ENV_PREFIXES, isSecretEnvName } from "./command-class.js";
|
|
51
|
+
export { NON_SECRET_ENV_NAMES, SECRET_ENV_PREFIXES, isSecretEnvName };
|
|
52
|
+
export interface ChildEnvironmentOptions {
|
|
53
|
+
/** The environment to start from. Defaults to this process's own. */
|
|
54
|
+
readonly source?: NodeJS.ProcessEnv;
|
|
55
|
+
/**
|
|
56
|
+
* The name the policy's `vault.passphrase_env` gives, when a policy was
|
|
57
|
+
* loaded. `null` or omitted removes nothing beyond the prefixed family.
|
|
58
|
+
*/
|
|
59
|
+
readonly passphraseEnv?: string | null;
|
|
60
|
+
/**
|
|
61
|
+
* The credential names the granted action's adapter declared in
|
|
62
|
+
* `requiredCredentials` (APRV-169). Passed through even when they fall under
|
|
63
|
+
* the credential-bearing prefixes: the adapter said it cannot act without
|
|
64
|
+
* them, and this is the injection point the design names.
|
|
65
|
+
*/
|
|
66
|
+
readonly declaredCredentials?: readonly string[];
|
|
67
|
+
}
|
|
68
|
+
export interface ChildEnvironment {
|
|
69
|
+
/** What to hand `spawnSync`. Never a reference to the source. */
|
|
70
|
+
readonly env: Record<string, string>;
|
|
71
|
+
/** How many variables were withheld. Names are deliberately not reported. */
|
|
72
|
+
readonly stripped: number;
|
|
73
|
+
/**
|
|
74
|
+
* The declared names that were present in the source and survived, as a
|
|
75
|
+
* count, for the same reason: a caller may want to know that injection
|
|
76
|
+
* happened without learning what was injected.
|
|
77
|
+
*/
|
|
78
|
+
readonly passed: number;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Build the environment a granted child gets.
|
|
82
|
+
*
|
|
83
|
+
* Total and deterministic: no reads, no throws, and the same source produces
|
|
84
|
+
* the same answer every time. A variable whose value is `undefined` (Node's
|
|
85
|
+
* spelling for "unset") is neither copied nor counted, because there was
|
|
86
|
+
* nothing there to withhold.
|
|
87
|
+
*/
|
|
88
|
+
export declare function childEnvironment(options?: ChildEnvironmentOptions): ChildEnvironment;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The write-boundary clock (amended SPEC.md §8, APRV-20 pass two / amendment A2).
|
|
3
|
+
*
|
|
4
|
+
* ## Why this module exists
|
|
5
|
+
*
|
|
6
|
+
* "Events written through the gate (`approval.*`, `execution.*`, `budget.*`,
|
|
7
|
+
* `audit.*`, `policy.updated`) have `ts` assigned by the runtime at the write
|
|
8
|
+
* boundary. Caller-supplied timestamps on these types MUST be refused. Because
|
|
9
|
+
* TTL judgment and budget windows read `ts`, a party subject to those controls
|
|
10
|
+
* must never author the clock they are judged by."
|
|
11
|
+
*
|
|
12
|
+
* Before this amendment every gate-typed append took `ts` as a positional
|
|
13
|
+
* parameter, so an agent calling the core (or the CLI's own argv, had a flag
|
|
14
|
+
* ever been added) could hand the runtime the moment it wished to be judged at:
|
|
15
|
+
* a timestamp inside a lapsed TTL, or one that placed an authorization outside
|
|
16
|
+
* the rolling budget window. The refusal the spec asks for is expressed here
|
|
17
|
+
* **structurally rather than as a check**: the parameter no longer exists on any
|
|
18
|
+
* public gate/token/execute/attest function, so there is nothing to refuse and
|
|
19
|
+
* nothing to forget to refuse.
|
|
20
|
+
*
|
|
21
|
+
* Determinism is preserved by injection rather than by parameters. Every such
|
|
22
|
+
* function takes an optional `clock` in its options; the CLI never passes one
|
|
23
|
+
* (so the real clock is read once, at the write boundary, inside core), and
|
|
24
|
+
* tests pass a fixed clock so TTL lapse and budget windows stay exercised
|
|
25
|
+
* without sleeps. A replay still reproduces exactly, because the clock is an
|
|
26
|
+
* input to the run rather than a read of ambient state inside the hashing path.
|
|
27
|
+
*
|
|
28
|
+
* ## The carve-out
|
|
29
|
+
*
|
|
30
|
+
* `core/log.ts`'s `appendEvent` still accepts `ts`, deliberately. SPEC.md §8
|
|
31
|
+
* leaves direct log writers outside the gate free to supply their own
|
|
32
|
+
* timestamps — an importer replaying a historical log is the obvious case, and
|
|
33
|
+
* a writer that could not state when something happened could not import
|
|
34
|
+
* anything. The rule binds the *gate*, which is where a subject of oversight
|
|
35
|
+
* would benefit from lying.
|
|
36
|
+
*/
|
|
37
|
+
/** A source of RFC 3339 instants. Injected, never read from ambient state. */
|
|
38
|
+
export type Clock = () => string;
|
|
39
|
+
/** The default: the real clock, read at the write boundary and nowhere else. */
|
|
40
|
+
export declare const systemClock: Clock;
|
|
41
|
+
/** Options carrying an injectable clock. Shared by every gate-typed writer. */
|
|
42
|
+
export interface ClockOptions {
|
|
43
|
+
/**
|
|
44
|
+
* The clock the runtime stamps this write with. Defaults to
|
|
45
|
+
* {@link systemClock}. Tests inject a fixed clock; production does not pass
|
|
46
|
+
* one at all, so the timestamp of a gate event is authored by the runtime and
|
|
47
|
+
* never by the party being judged (amended SPEC.md §8).
|
|
48
|
+
*/
|
|
49
|
+
clock?: Clock;
|
|
50
|
+
}
|
|
51
|
+
/** Read the injected clock, or the real one. One line, one place. */
|
|
52
|
+
export declare function tick(options?: ClockOptions): string;
|