wicked-vault 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 +176 -0
- package/bin/wicked-vault.mjs +161 -0
- package/docs/CONTRACTS.md +421 -0
- package/docs/adr/0001-standalone-and-council-revisions.md +101 -0
- package/docs/adr/0002-independent-evaluation-and-criteria-binding.md +184 -0
- package/install.mjs +192 -0
- package/package.json +52 -0
- package/skills/wicked-vault/analyze-evidence/SKILL.md +119 -0
- package/skills/wicked-vault/cross-check-evidence/SKILL.md +141 -0
- package/skills/wicked-vault/init/SKILL.md +58 -0
- package/skills/wicked-vault/record-evidence/SKILL.md +129 -0
- package/skills/wicked-vault/verify-evidence/SKILL.md +76 -0
- package/src/bus.mjs +75 -0
- package/src/hash.mjs +40 -0
- package/src/id.mjs +9 -0
- package/src/vault.mjs +425 -0
- package/src/verifiers.mjs +84 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wicked-vault:cross-check-evidence
|
|
3
|
+
description: Declare a consumer-authored contract and get a mechanical PASS/REJECT verdict for a scope+phase by re-deriving every required artifact. Use when answering "is this claim actually backed by evidence that still holds?" — gate logic, release readiness, or merge checks. Fail-closed when no contract is declared.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# wicked-vault:cross-check-evidence
|
|
7
|
+
|
|
8
|
+
Evaluate a whole **contract** — the set of evidence a scope+phase requires — and
|
|
9
|
+
return a single mechanical verdict. Cross-check re-derives every required
|
|
10
|
+
artifact (it does not trust cached statuses) and reports `PASS` only when all of
|
|
11
|
+
them hold.
|
|
12
|
+
|
|
13
|
+
This is the "is the work *backed*?" question. The contract is **consumer-
|
|
14
|
+
authored**; the vault only decides *whether* it's satisfied, never *what* it
|
|
15
|
+
should require (G9). The vault has no gate logic of its own to leak.
|
|
16
|
+
|
|
17
|
+
## When to use
|
|
18
|
+
|
|
19
|
+
- A gate / release-readiness / merge check that aggregates several claims.
|
|
20
|
+
- Answering: "is this claim actually backed by evidence that still holds?"
|
|
21
|
+
- Any time a stored "ready to merge" / "tests pass" must be re-proven, not
|
|
22
|
+
asserted.
|
|
23
|
+
|
|
24
|
+
For a single artifact, use `wicked-vault:verify-evidence` instead.
|
|
25
|
+
|
|
26
|
+
## Step 1 — Declare the contract
|
|
27
|
+
|
|
28
|
+
Write a JSON spec listing the required evidence, then declare it for a
|
|
29
|
+
scope+phase. A claim with a `verifier` pin also constrains how its evidence may
|
|
30
|
+
be recorded (G8 — see `wicked-vault:record-evidence`).
|
|
31
|
+
|
|
32
|
+
```jsonc
|
|
33
|
+
// contract.json
|
|
34
|
+
{
|
|
35
|
+
"required_evidence": [
|
|
36
|
+
{ "claim_id": "tests-pass", "kind": "test-run", "criteria": "all unit tests pass (exit 0)", "verifier": { "kind": "exit_code_eq" } },
|
|
37
|
+
{ "claim_id": "no-secrets", "kind": "test-run", "criteria": "no secrets in the diff", "verifier": { "kind": "not_contains" } },
|
|
38
|
+
{ "claim_id": "design-ok", "kind": "review-verdict", "criteria": "the change adequately addresses the documented failure modes", "require_attestation": true },
|
|
39
|
+
{ "claim_id": "changelog", "kind": "file", "required": false }
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npx wicked-vault declare-contract --scope checkout --phase release --spec contract.json
|
|
46
|
+
# -> { "contract_version": "<16-char hash>" }
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
- `required: false` makes a claim optional — its absence is `PASS`, not `MISSING`.
|
|
50
|
+
- **`criteria`** pins the acceptance criteria for the claim. This is the
|
|
51
|
+
**trusted path** — criteria authored in the contract (separately from the
|
|
52
|
+
worker), so a recorded artifact must match it (`criteria_authored_by:
|
|
53
|
+
contract`). Strongly preferred over worker-supplied criteria.
|
|
54
|
+
- **`require_attestation: true`** marks a claim that needs an independent
|
|
55
|
+
judgment (the judgment tier) — see Step 4. Use it for free-form criteria a
|
|
56
|
+
deterministic verifier can't express ("adequately addresses the failure
|
|
57
|
+
modes").
|
|
58
|
+
- The `contract_version` is a hash of the required-evidence set (G8 pinning).
|
|
59
|
+
|
|
60
|
+
## Step 2 — Record the evidence
|
|
61
|
+
|
|
62
|
+
Record one artifact per required claim (`wicked-vault:record-evidence`). Each `claim_id`
|
|
63
|
+
must match the contract, and `--criteria` must match the contract's pin. The
|
|
64
|
+
**latest active** artifact for a claim wins.
|
|
65
|
+
|
|
66
|
+
## Step 3 — Cross-check (integrity tier — the default, CI-safe)
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npx wicked-vault cross-check --scope checkout --phase release
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
This is **`--integrity-only`** by default: deterministic, offline, no model
|
|
73
|
+
calls — safe to put on a CI gate. It evaluates hash integrity + any
|
|
74
|
+
deterministic verifier per claim.
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"scope": "checkout", "phase": "release",
|
|
79
|
+
"contract_version": "...",
|
|
80
|
+
"overall": "PASS",
|
|
81
|
+
"claims": [
|
|
82
|
+
{ "claim_id": "tests-pass", "artifact_id": "...", "hash_ok": true, "verifier_status": "pass", "result": "PASS", "detail": "exit_code=0" },
|
|
83
|
+
{ "claim_id": "no-secrets", "artifact_id": "...", "hash_ok": true, "verifier_status": "pass", "result": "PASS", "detail": "/(?i)secret/ absent" }
|
|
84
|
+
],
|
|
85
|
+
"evaluated_at": "..."
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Verdicts and exit code
|
|
90
|
+
|
|
91
|
+
Exit `0` **iff** `overall === "PASS"`. Per-claim `result` is one of:
|
|
92
|
+
|
|
93
|
+
| result | meaning |
|
|
94
|
+
|---|---|
|
|
95
|
+
| `PASS` | required artifact present, hash intact, verifier passed (and — in `--with-attestations` — an independent `pass` opinion when `require_attestation`) |
|
|
96
|
+
| `MISSING` | a required claim has no active artifact |
|
|
97
|
+
| `FAIL` | artifact present but tamper or verifier failed |
|
|
98
|
+
| `UNATTESTED` | `require_attestation` claim has no independent opinion recorded |
|
|
99
|
+
| `REJECT` | `require_attestation` claim has a non-pass / stale independent opinion |
|
|
100
|
+
| `ERROR` | verifier-kind pin mismatch against the contract |
|
|
101
|
+
|
|
102
|
+
`overall` is `PASS` only if every claim is `PASS`; `ERROR` if any claim errored;
|
|
103
|
+
otherwise `REJECT`.
|
|
104
|
+
|
|
105
|
+
## Step 4 — Judgment tier (opt-in, NOT for the default CI gate)
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
npx wicked-vault cross-check --scope checkout --phase release --with-attestations
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`--with-attestations` consults the latest independent opinion per claim (the
|
|
112
|
+
attestations recorded by `wicked-vault:analyze-evidence`). For a claim with
|
|
113
|
+
`require_attestation: true`, it `PASS`es only when integrity passes **and** a
|
|
114
|
+
non-stale, independent `pass` opinion exists; otherwise `UNATTESTED` / `REJECT`.
|
|
115
|
+
For other claims the opinion is advisory (surfaced, doesn't change the result).
|
|
116
|
+
|
|
117
|
+
This mode is **not deterministic and not for the default gate** — opinions come
|
|
118
|
+
from a model and are point-in-time. Run `wicked-vault:analyze-evidence` first to
|
|
119
|
+
produce the attestations, then use this mode for a release sign-off that
|
|
120
|
+
requires a third-party judgment. Keep `--integrity-only` on the fast CI path.
|
|
121
|
+
|
|
122
|
+
## Fail-closed (G5)
|
|
123
|
+
|
|
124
|
+
If **no contract is declared** for the scope+phase, cross-check returns
|
|
125
|
+
`overall: "ERROR"` and a non-zero exit — it never reports PASS by default. An
|
|
126
|
+
undeclared expectation can't be silently satisfied.
|
|
127
|
+
|
|
128
|
+
## wicked-bus event
|
|
129
|
+
|
|
130
|
+
If wicked-bus is installed, `cross-check` publishes `wicked.contract.checked`
|
|
131
|
+
(domain `wicked-vault`, subdomain `vault.cross_check`) carrying the `overall`
|
|
132
|
+
verdict — this is the signal a gate consumer subscribes to. A detected tamper
|
|
133
|
+
also publishes `wicked.evidence.tampered`. `declare-contract` publishes
|
|
134
|
+
`wicked.contract.declared`. Emission is fire-and-forget and a no-op when the bus
|
|
135
|
+
is absent or `WICKED_VAULT_NO_BUS=1`.
|
|
136
|
+
|
|
137
|
+
## Inspecting what's recorded
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
npx wicked-vault list --scope checkout --phase release # all entries (active + superseded)
|
|
141
|
+
```
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wicked-vault:init
|
|
3
|
+
description: Initialize a wicked-vault in a repository so claims can be backed by re-derivable evidence. Use when setting up the vault for the first time, when a vault command reports "no .wicked-vault/ found", or before the first record/cross-check in a project.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# wicked-vault:init
|
|
7
|
+
|
|
8
|
+
Set up the local-first **evidence primitive** in the current repository. The
|
|
9
|
+
vault records claim-backing artifacts, hashes them tamper-evidently, and
|
|
10
|
+
*re-derives* their verdict on demand — it never trusts a stored status.
|
|
11
|
+
|
|
12
|
+
## When to use
|
|
13
|
+
|
|
14
|
+
- First time using the vault in a repo.
|
|
15
|
+
- A command failed with `no .wicked-vault/ found; run \`wicked-vault init\``.
|
|
16
|
+
- Before the first `record` or `declare-contract` in a project.
|
|
17
|
+
|
|
18
|
+
## Initialize
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx wicked-vault init
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
This creates `.wicked-vault/` at the repo root with:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
.wicked-vault/
|
|
28
|
+
vault.json # schema_version, store_mode: in-repo, payload_max_bytes
|
|
29
|
+
entries/ # one JSON envelope per recorded artifact (append-only)
|
|
30
|
+
payloads/ # content-addressed payload blobs (sha256-named, deduped)
|
|
31
|
+
contracts/ # consumer-authored contracts, per scope/phase
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`record`, `declare-contract`, and `supersede` auto-create the vault if one
|
|
35
|
+
isn't found, so explicit `init` is mostly for clarity. `verify`, `cross-check`,
|
|
36
|
+
and `list` do **not** auto-create — they fail-closed when no vault exists.
|
|
37
|
+
|
|
38
|
+
The vault is discovered by walking up from the current directory, so any
|
|
39
|
+
subdirectory of the repo can run vault commands.
|
|
40
|
+
|
|
41
|
+
## Should this be committed?
|
|
42
|
+
|
|
43
|
+
`store_mode` defaults to `in-repo` — the vault lives inside the working tree
|
|
44
|
+
and git becomes the audit chain. Decide per project whether `.wicked-vault/` is
|
|
45
|
+
committed (shared, auditable evidence) or git-ignored (local-only scratch). The
|
|
46
|
+
repo's own `.gitignore` ignores `.wicked-vault/` by default; remove that line to
|
|
47
|
+
commit evidence.
|
|
48
|
+
|
|
49
|
+
## Next steps
|
|
50
|
+
|
|
51
|
+
- `wicked-vault:record-evidence` — capture an artifact and attach a verifier.
|
|
52
|
+
- `wicked-vault:cross-check-evidence` — declare a contract and get a mechanical verdict.
|
|
53
|
+
|
|
54
|
+
## Output
|
|
55
|
+
|
|
56
|
+
Every command emits JSON to stdout and uses the **exit code as the gate
|
|
57
|
+
signal** (`0` = PASS / success). `init` returns the absolute path of the
|
|
58
|
+
created `.wicked-vault/` directory.
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wicked-vault:record-evidence
|
|
3
|
+
description: Record a claim-backing artifact in the vault and attach a deterministic verifier. Use when capturing evidence that "tests pass", "build clean", a commit exists, or a file's contents back a claim — and when replacing stale evidence via supersede. Covers --run vs --artifact, verifier syntax, and contract pinning.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# wicked-vault:record-evidence
|
|
7
|
+
|
|
8
|
+
Capture an artifact, hash it tamper-evidently, and attach a verifier that can
|
|
9
|
+
**re-derive** its verdict later. The vault does the capture itself — it never
|
|
10
|
+
trusts a claimed status (G4).
|
|
11
|
+
|
|
12
|
+
## When to use
|
|
13
|
+
|
|
14
|
+
- Backing a claim with evidence: "tests pass", "build clean", "no secrets",
|
|
15
|
+
"commit landed", "config has the required field".
|
|
16
|
+
- Replacing stale evidence with a fresh artifact (use **supersede**, below).
|
|
17
|
+
|
|
18
|
+
## Two ways to capture
|
|
19
|
+
|
|
20
|
+
### 1. Run a command and capture its result (`--run`)
|
|
21
|
+
|
|
22
|
+
The vault executes the source command and stores `{command, exit_code, stdout,
|
|
23
|
+
stderr, captured_at}` as the payload.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx wicked-vault record \
|
|
27
|
+
--scope checkout --phase build --claim tests-pass --kind test-run \
|
|
28
|
+
--source "npm test" --criteria "all unit tests pass (exit 0)" \
|
|
29
|
+
--verifier "exit_code_eq:0" --run
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### 2. Hash an existing file (`--artifact`)
|
|
33
|
+
|
|
34
|
+
The vault reads the file and stores its bytes as the payload.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npx wicked-vault record \
|
|
38
|
+
--scope checkout --phase build --claim coverage-report --kind file \
|
|
39
|
+
--source "coverage/summary.json" --artifact coverage/summary.json \
|
|
40
|
+
--criteria "line coverage is at least 80%" \
|
|
41
|
+
--verifier "jq_pred:.total.lines.pct >= 80"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`record` requires **either** `--run` **or** `--artifact`, and **always**
|
|
45
|
+
`--criteria`.
|
|
46
|
+
|
|
47
|
+
## Required fields
|
|
48
|
+
|
|
49
|
+
| Flag | Meaning |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `--scope` | the unit the claim is about (e.g. a service, module, PR) |
|
|
52
|
+
| `--phase` | lifecycle phase (e.g. `build`, `review`, `release`) |
|
|
53
|
+
| `--claim` | claim id this artifact backs (e.g. `tests-pass`) |
|
|
54
|
+
| `--kind` | artifact kind (e.g. `test-run`, `file`, `commit`) |
|
|
55
|
+
| `--source` | the command (`--run`) or path/description (`--artifact`) |
|
|
56
|
+
| `--criteria` | **mandatory** — the acceptance criteria this evidence claims to clear; inline text or `@file`. Hashed into the envelope and frozen to the evidence (G10) |
|
|
57
|
+
| `--verifier` | *optional* deterministic sub-check (see below) — a composable signal an independent evaluator can cite |
|
|
58
|
+
|
|
59
|
+
## Acceptance criteria are mandatory (G10/D1)
|
|
60
|
+
|
|
61
|
+
Every artifact must state the bar it claims to clear — `record` rejects evidence
|
|
62
|
+
with no `--criteria`. The criteria are hashed into the envelope, so the bar is
|
|
63
|
+
**frozen to the evidence**: the same evidence can never later be judged against
|
|
64
|
+
weaker criteria (anti-downgrade).
|
|
65
|
+
|
|
66
|
+
The **trusted path** is contract-pinned criteria: when `declare-contract` pins
|
|
67
|
+
`criteria` for the claim, a matching `--criteria` is stamped
|
|
68
|
+
`criteria_authored_by: contract`. Worker-supplied criteria are stamped
|
|
69
|
+
`record` (a weaker provenance class — see `wicked-vault:analyze-evidence`'s
|
|
70
|
+
threat model). A `--criteria` that contradicts a contract pin is a G8 downgrade
|
|
71
|
+
and is rejected.
|
|
72
|
+
|
|
73
|
+
The independent judgment of *whether the evidence meets these criteria* is the
|
|
74
|
+
job of `wicked-vault:analyze-evidence` (the judgment tier), not `record`.
|
|
75
|
+
|
|
76
|
+
## Verifier syntax
|
|
77
|
+
|
|
78
|
+
`--verifier "kind:arg"` (or a JSON object for advanced params). The v1 core
|
|
79
|
+
verifiers are **deterministic and pure** (G7):
|
|
80
|
+
|
|
81
|
+
| Verifier | Example | Passes when |
|
|
82
|
+
|---|---|---|
|
|
83
|
+
| `exit_code_eq` | `exit_code_eq:0` | captured exit code equals N (requires `--run`) |
|
|
84
|
+
| `regex_match` | `regex_match:[0-9a-f]{40}` | pattern matches stdout+stderr / file text |
|
|
85
|
+
| `not_contains` | `not_contains:(?i)error` | pattern is **absent** |
|
|
86
|
+
| `jq_pred` | `jq_pred:.ok == true` | `jq -e` on the JSON payload is truthy (needs `jq`) |
|
|
87
|
+
| `commit_exists` | `commit_exists:<sha>` | the git commit exists in the repo |
|
|
88
|
+
|
|
89
|
+
`llm_eval` is intentionally **not** a verifier kind — a nondeterministic judge
|
|
90
|
+
would falsify the purity guarantee (G7).
|
|
91
|
+
|
|
92
|
+
## Output
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{ "id": "...", "envelope_hash": "...", "status_at_record": "pass", "status_detail": "exit_code=0" }
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`status_at_record` is **informational only** — `verify` and `cross-check` never
|
|
99
|
+
read it; they re-run the verifier (G3). The exit code is `0` on a successful
|
|
100
|
+
record (the recording succeeded), regardless of the verdict — to gate on the
|
|
101
|
+
verdict, use `verify` or `cross-check`.
|
|
102
|
+
|
|
103
|
+
## wicked-bus event
|
|
104
|
+
|
|
105
|
+
If wicked-bus is installed, `record` publishes `wicked.evidence.recorded`
|
|
106
|
+
(domain `wicked-vault`, subdomain `vault.record`) fire-and-forget. `supersede`
|
|
107
|
+
publishes `wicked.evidence.superseded`. Emission is a silent no-op when the bus
|
|
108
|
+
is absent or `WICKED_VAULT_NO_BUS=1` — it never affects the output or exit code.
|
|
109
|
+
|
|
110
|
+
## Replacing evidence (supersede)
|
|
111
|
+
|
|
112
|
+
Evidence is append-only (G6). To replace a stale artifact, record a replacement
|
|
113
|
+
and link it — the old entry is flipped to `superseded`, never deleted:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
npx wicked-vault supersede <old-id> \
|
|
117
|
+
--scope checkout --phase build --claim tests-pass --kind test-run \
|
|
118
|
+
--source "npm test" --verifier "exit_code_eq:0" --run
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Crash-safe ordering: the replacement is written and confirmed on disk before
|
|
122
|
+
the old entry is flipped, so there is always an active artifact for the claim.
|
|
123
|
+
|
|
124
|
+
## Contract pinning (G8)
|
|
125
|
+
|
|
126
|
+
If a contract pins this claim (see `wicked-vault:cross-check-evidence`), `record` rejects
|
|
127
|
+
a downgrade — a `kind`, `source`, or `verifier` that differs from the pin throws
|
|
128
|
+
a `G8 pin violation`. This stops a weaker verifier from being swapped in to make
|
|
129
|
+
a claim pass.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wicked-vault:verify-evidence
|
|
3
|
+
description: Deterministically re-derive a single recorded artifact's integrity — recompute the payload/criteria/envelope hashes and re-run its pure verifier. Model-free, reproducible, CI-gate-safe. Use to confirm a piece of evidence is still intact and its deterministic check passes, or to detect tamper. Never trusts the cached status. For an INDEPENDENT judgment of whether the evidence MEETS its criteria, use wicked-vault:analyze-evidence.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# wicked-vault:verify-evidence
|
|
7
|
+
|
|
8
|
+
The **integrity tier** (G1–G9). Re-derive the verdict for **one** recorded
|
|
9
|
+
artifact by id: recompute the payload, criteria, and envelope hashes from the
|
|
10
|
+
stored bytes and **re-run the pure verifier** — never reading the status stored
|
|
11
|
+
at record time (G3). Deterministic, offline, no model — safe on a CI gate.
|
|
12
|
+
|
|
13
|
+
**This is not the judgment tier.** It answers *"is this artifact intact and does
|
|
14
|
+
its deterministic check still pass?"* — not *"does this evidence actually meet
|
|
15
|
+
its acceptance criteria?"* For the latter (an independent model analysis of
|
|
16
|
+
evidence-vs-criteria), use **`wicked-vault:analyze-evidence`**.
|
|
17
|
+
|
|
18
|
+
## When to use
|
|
19
|
+
|
|
20
|
+
- Confirming a specific artifact still holds *right now* (cheap, reproducible).
|
|
21
|
+
- Detecting tamper: a payload, criteria, or envelope field modified after
|
|
22
|
+
recording.
|
|
23
|
+
- The integrity precheck before an independent analysis or a gate.
|
|
24
|
+
|
|
25
|
+
To check a whole contract at once, use `wicked-vault:cross-check-evidence`.
|
|
26
|
+
|
|
27
|
+
## Verify
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx wicked-vault verify <artifact-id>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Get artifact ids from `record` output or `npx wicked-vault list --scope <S>`.
|
|
34
|
+
|
|
35
|
+
## Output
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"id": "...",
|
|
40
|
+
"hash_ok": true,
|
|
41
|
+
"payload_ok": true,
|
|
42
|
+
"criteria_ok": true,
|
|
43
|
+
"envelope_ok": true,
|
|
44
|
+
"status": "pass",
|
|
45
|
+
"rederived": true,
|
|
46
|
+
"detail": "exit_code=0",
|
|
47
|
+
"ignored_cached_status": "pass",
|
|
48
|
+
"latest_attestation": { "opinion": "pass", "evaluator": "gemini-reviewer", "stale": false }
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
| Field | Meaning |
|
|
53
|
+
|---|---|
|
|
54
|
+
| `hash_ok` | payload, criteria, and envelope hashes all match what was stored |
|
|
55
|
+
| `payload_ok` / `criteria_ok` / `envelope_ok` | which part of the tamper check passed |
|
|
56
|
+
| `status` | re-derived integrity verdict: `pass` / `fail` / `error` |
|
|
57
|
+
| `rederived` | always `true` — proves the check was actually re-run |
|
|
58
|
+
| `ignored_cached_status` | the stored status the vault refused to trust |
|
|
59
|
+
| `latest_attestation` | the most recent **independent opinion** (from `analyze-evidence`), shown **for reference only** — not re-derived, not trusted as reproducible; `stale: true` if it judged different bytes |
|
|
60
|
+
|
|
61
|
+
## Exit code is the gate
|
|
62
|
+
|
|
63
|
+
Exit `0` **iff** `hash_ok && status === "pass"`. Any tamper or a failing
|
|
64
|
+
verifier exits non-zero — script against the exit code:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
if npx wicked-vault verify "$id" >/dev/null; then echo "still intact"; fi
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Fail-closed (G5)
|
|
71
|
+
|
|
72
|
+
A pass requires an intact hash **and** (if the artifact carries a verifier) a
|
|
73
|
+
passing verifier. If any hash diverges, `status` is forced to `fail` with a
|
|
74
|
+
`TAMPER:` detail. A missing entry or payload blob returns `status: "error"` —
|
|
75
|
+
never a silent pass. The `latest_attestation` is informational and never affects
|
|
76
|
+
this exit code (an independent opinion is a separate, non-reproducible tier).
|
package/src/bus.mjs
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Optional, fire-and-forget wicked-bus integration.
|
|
2
|
+
//
|
|
3
|
+
// The vault is a zero-dependency, local-first primitive. wicked-bus is a
|
|
4
|
+
// *sibling* primitive, never a hard dependency: this module dynamic-imports it
|
|
5
|
+
// at runtime and degrades to a silent no-op when it is absent or disabled.
|
|
6
|
+
// Emission NEVER blocks or breaks a vault command — by the time we publish, the
|
|
7
|
+
// evidence write has already happened (G6 append-only), and a bus problem must
|
|
8
|
+
// not change a verdict, the stdout JSON, or an exit code.
|
|
9
|
+
//
|
|
10
|
+
// Opt out entirely with WICKED_VAULT_NO_BUS=1.
|
|
11
|
+
|
|
12
|
+
import { createRequire } from 'node:module';
|
|
13
|
+
import { pathToFileURL } from 'node:url';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
|
|
16
|
+
const DOMAIN = 'wicked-vault';
|
|
17
|
+
|
|
18
|
+
// Resolve the wicked-bus module namespace, or null. Two layers, in order:
|
|
19
|
+
// 1. bare specifier — works when wicked-bus is installed globally or hoisted
|
|
20
|
+
// alongside the vault.
|
|
21
|
+
// 2. the consumer project's node_modules (anchored at cwd) — the common case,
|
|
22
|
+
// since sibling tools live in the same repo the vault is invoked from, and
|
|
23
|
+
// the vault itself ships no node_modules.
|
|
24
|
+
async function resolveBus(cwd) {
|
|
25
|
+
try {
|
|
26
|
+
return await import('wicked-bus');
|
|
27
|
+
} catch {
|
|
28
|
+
/* fall through to layer 2 */
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const require = createRequire(join(cwd, '__vault_bus_anchor__.js'));
|
|
32
|
+
const entry = require.resolve('wicked-bus');
|
|
33
|
+
return await import(pathToFileURL(entry).href);
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Build the bus publisher.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} [cwd] directory to anchor module resolution from.
|
|
43
|
+
* @returns {Promise<(event_type: string, subdomain: string, payload: object) => void>}
|
|
44
|
+
* a synchronous publish closure, or a no-op when the bus is unavailable or
|
|
45
|
+
* disabled. The returned function never throws.
|
|
46
|
+
*/
|
|
47
|
+
export async function initBus(cwd = process.cwd()) {
|
|
48
|
+
const NOOP = () => {};
|
|
49
|
+
if (process.env.WICKED_VAULT_NO_BUS === '1') return NOOP;
|
|
50
|
+
|
|
51
|
+
const bus = await resolveBus(cwd);
|
|
52
|
+
if (!bus || typeof bus.emit !== 'function' || typeof bus.openDb !== 'function') {
|
|
53
|
+
return NOOP;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let db;
|
|
57
|
+
let config;
|
|
58
|
+
try {
|
|
59
|
+
config = typeof bus.loadConfig === 'function' ? bus.loadConfig() : {};
|
|
60
|
+
// openDb opens (and idempotently initializes) the local bus db — this is
|
|
61
|
+
// wicked-bus's own auto-init model, the same path the wicked-brain
|
|
62
|
+
// installer uses.
|
|
63
|
+
db = bus.openDb(config);
|
|
64
|
+
} catch {
|
|
65
|
+
return NOOP;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return (event_type, subdomain, payload) => {
|
|
69
|
+
try {
|
|
70
|
+
bus.emit(db, config, { event_type, domain: DOMAIN, subdomain, payload });
|
|
71
|
+
} catch {
|
|
72
|
+
// swallow — a bus error must never break a vault command
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
package/src/hash.mjs
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
export function sha256(buf) {
|
|
4
|
+
return createHash('sha256').update(buf).digest('hex');
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// Deterministic canonical JSON (recursively key-sorted, no whitespace) so a
|
|
8
|
+
// hash over a structure is stable regardless of key order.
|
|
9
|
+
function sortKeys(o) {
|
|
10
|
+
if (o === null || typeof o !== 'object') return o;
|
|
11
|
+
if (Array.isArray(o)) return o.map(sortKeys);
|
|
12
|
+
const out = {};
|
|
13
|
+
for (const k of Object.keys(o).sort()) out[k] = sortKeys(o[k]);
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function canonical(obj) {
|
|
18
|
+
return JSON.stringify(sortKeys(obj));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// G2 — the envelope hash binds the identifying tuple (now including the
|
|
22
|
+
// acceptance-criteria hash) to the payload hash. Mutating ANY of these fields,
|
|
23
|
+
// the criteria, or the payload changes the envelope, so a later `verify`
|
|
24
|
+
// recomputation will diverge from the stored value. The verifier is optional
|
|
25
|
+
// (ADR-0002: it became a composable sub-check, not the whole story).
|
|
26
|
+
export function envelopeHash(fields) {
|
|
27
|
+
const tuple = {
|
|
28
|
+
scope: fields.scope,
|
|
29
|
+
phase: fields.phase,
|
|
30
|
+
claim_id: fields.claim_id,
|
|
31
|
+
kind: fields.kind,
|
|
32
|
+
source: fields.source,
|
|
33
|
+
verifier: fields.verifier
|
|
34
|
+
? { kind: fields.verifier.kind, params: fields.verifier.params || {} }
|
|
35
|
+
: null,
|
|
36
|
+
criteria_sha256: fields.criteria_sha256,
|
|
37
|
+
payload_sha256: fields.payload_sha256,
|
|
38
|
+
};
|
|
39
|
+
return sha256(Buffer.from(canonical(tuple), 'utf8'));
|
|
40
|
+
}
|
package/src/id.mjs
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
// G1 — server-minted id. Time-prefixed hex so ids sort by creation order
|
|
4
|
+
// (ULID-grade ordering; not strict ULID encoding). Callers cannot supply it.
|
|
5
|
+
export function newId() {
|
|
6
|
+
const t = Date.now().toString(16).padStart(12, '0');
|
|
7
|
+
const r = randomBytes(8).toString('hex');
|
|
8
|
+
return (t + r).toUpperCase();
|
|
9
|
+
}
|