Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 9 additions & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ resolver = "3"
members = ["crates/*"]

[workspace.package]
version = "0.1.5"
version = "0.2.0"
edition = "2024"
rust-version = "1.99"
repository = "https://github.com/dekrops/verified-cli"
Expand Down Expand Up @@ -35,14 +35,14 @@ codegen-units = 1
strip = "symbols"

[workspace.dependencies]
verified-canonical = { path = "crates/verified-canonical", version = "0.1.5" }
verified-contract = { path = "crates/verified-contract", version = "0.1.5" }
verified-derivation = { path = "crates/verified-derivation", version = "0.1.5" }
verified-history = { path = "crates/verified-history", version = "0.1.5" }
verified-protocol = { path = "crates/verified-protocol", version = "0.1.5" }
verified-verifier = { path = "crates/verified-verifier", version = "0.1.5" }
verified-work-unit = { path = "crates/verified-work-unit", version = "0.1.5" }
verified-workspace = { path = "crates/verified-workspace", version = "0.1.5" }
verified-canonical = { path = "crates/verified-canonical", version = "0.2.0" }
verified-contract = { path = "crates/verified-contract", version = "0.2.0" }
verified-derivation = { path = "crates/verified-derivation", version = "0.2.0" }
verified-history = { path = "crates/verified-history", version = "0.2.0" }
verified-protocol = { path = "crates/verified-protocol", version = "0.2.0" }
verified-verifier = { path = "crates/verified-verifier", version = "0.2.0" }
verified-work-unit = { path = "crates/verified-work-unit", version = "0.2.0" }
verified-workspace = { path = "crates/verified-workspace", version = "0.2.0" }

anstream = "1.0.0"
anstyle = "1.0.14"
Expand Down
31 changes: 23 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

```console
$ verified verify examples/invoice-posting/supported.json \
--expect-contract sha-256:6a2c3b0bf5814aacd8c699f430db0a11b50de812d3dd86f30823aa98df46b550 \
--trust examples/invoice-posting/trust.json
Work Verification Protocol package (wvp-0.2, draft)
package wvp:sha-256:d0982b73f310…
Expand All @@ -34,6 +35,7 @@ decision reproduced SUPPORTED, matches the package
POSTED TRUE
issuer signature absent
source truthfulness NOT established: no check can establish it
expectations met (contract)

complete: every protocol check ran; the decision SUPPORTED was reproduced
```
Expand Down Expand Up @@ -69,6 +71,12 @@ that:
- the decision (`SUPPORTED`, `CONTRADICTED` or `UNDETERMINED`) is what the
contract yields from that evidence, recomputed on your machine.

The contract travels in the package, chosen by whoever made it. A contract
that requires nothing reproduces as `SUPPORTED` like any other, so the
decision is about the contract you agreed only when you pin it:
`--expect-contract` with its digest. Without a pin, the report says
`contract pinned NO`.

It cannot establish that a source told the truth, that a completeness
statement was honest, or that the package holds everything the sources know.
A valid signature proves who said something, not that it is so. And a
Expand Down Expand Up @@ -115,7 +123,8 @@ $ echo $?
| --- | --- |
| `--trust <REGISTRY>` | Your trust registry: the keys you accept, for which issuer, tenant, sources and period. Keys inside a package are never trusted. |
| `--expect-tenant`, `--expect-contract`, `--expect-claim` | Refuse a `wvp-0.2` package that verifies but is about another tenant, contract (by digest) or claim than you meant. |
| `--require-decision <DECISION>` | Exit 0 only if this decision (`SUPPORTED`, `CONTRADICTED` or `UNDETERMINED`) was reproduced; otherwise exit 4. Use it in scripts. |
| `--require-decision <DECISION>` | Exit 0 only if this decision (`SUPPORTED`, `CONTRADICTED` or `UNDETERMINED`) was reproduced, and for `wvp-0.2` under a contract pinned with `--expect-contract`; otherwise exit 4. Use it in scripts. |
| `--any-contract` | With `--require-decision`: accept the decision under whatever contract a `wvp-0.2` package carries. Say so only if any contract will do. |
| `--protocol <VERSION>` | Require a protocol version instead of detecting it. |
| `--json` | The same result as structured data, with full identifiers. |
| `--quiet` | Print nothing; rely on the exit code. |
Expand All @@ -127,7 +136,7 @@ $ echo $?
| 1 | Refused by a protocol check. `fsck`: damage found. `diff`: the packages differ. `replay`: a package was refused or is damaged. |
| 2 | The command could not run (usage, unreadable input, unusable registry, resource limit). |
| 3 | `verify`: not refused, but not every check ran (no registry). |
| 4 | `--require-decision` (`verify`, `replay`): every check ran, but a reproduced decision is not the required one. |
| 4 | `--require-decision` (`verify`, `replay`): every check ran, but a reproduced decision is not the required one, or a `wvp-0.2` contract was not pinned. |

### Gating a script on the decision

Expand All @@ -137,15 +146,21 @@ A script that acts on the exit code alone must therefore say which decision
it needs:

```sh
verified verify invoice.json --trust trust.json --require-decision SUPPORTED \
verified verify invoice.json --trust trust.json \
--expect-contract sha-256:<the digest you agreed> \
--require-decision SUPPORTED \
&& pay_invoice
```

With `--require-decision`, exit 0 means every check ran **and** that decision
was reproduced on this machine. Any other reproduced decision exits 4. The
decision a package merely carries never satisfies the requirement: without a
registry nothing is reproduced and the exit code is 3. The flag is opt-in;
without it the behaviour is unchanged.
was reproduced on this machine, under the contract you pinned. Any other
reproduced decision exits 4, and so does a `wvp-0.2` package whose contract
you did not pin: its producer chose that contract, and one that requires
nothing reproduces as `SUPPORTED`. `--any-contract` accepts the contract the
package carries, when that is really what you mean. The decision a package
merely carries never satisfies the requirement: without a registry nothing is
reproduced and the exit code is 3. Without `--require-decision` the exit code
is unchanged; the report says whether the contract was pinned.

A trust registry is a JSON file:

Expand Down Expand Up @@ -330,7 +345,7 @@ no damage found. Every stored package is the content its names commit to; that s
| --- | --- |
| `verified export <id> [-o FILE]` | Writes a stored package out byte for byte, to standard output or to a file that must not exist. Nothing is added: no signature, no result, no claim of this tool's own. |
| `verified diff <a> <b>` | Compares two packages (files or stored ones): which components differ and what the two state differently. Both need integrity; nothing else is checked. Exits 0 if the texts are identical, 1 if not. |
| `verified replay --trust REGISTRY [id…]` | Verifies stored packages again, now, under the registry given now. Nothing from an earlier run is reused. Exits 1 if any is refused or damaged; with `--require-decision`, 4 if any reproduces another decision. |
| `verified replay --trust REGISTRY [id…]` | Verifies stored packages again, now, under the registry given now. Nothing from an earlier run is reused. Exits 1 if any is refused or damaged; with `--require-decision`, 4 if any reproduces another decision or, unless `--any-contract`, is a `wvp-0.2` package whose contract `--expect-contract` did not pin. |
| `verified verify <package> --record FILE` | Refuses (`RECORD_MISMATCH`) a `wvp-0.2` package that a work record you hold is not about, naming every member that differs. |

A **work record** states one verification of one package: the claim, the
Expand Down
29 changes: 26 additions & 3 deletions crates/verified-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ Exit codes:
3 verify: the package was not refused, but not every check ran
registration log: the log does not reach the entry you hold
4 --require-decision: every check ran, but a reproduced decision is not
the required one
the required one, or a wvp-0.2 contract was not pinned
(--expect-contract; --any-contract accepts it unpinned)

The Work Verification Protocol is a draft. No check establishes that a
source told the truth.
Expand Down Expand Up @@ -630,9 +631,21 @@ pub(crate) struct Replay {
/// Your trust registry
#[arg(long, value_name = "REGISTRY")]
pub(crate) trust: PathBuf,
/// Exit 4 unless every package reproduces this decision
/// Exit 4 unless every package reproduces this decision under a pinned
/// contract
#[arg(long, value_name = "DECISION", value_parser = decision)]
pub(crate) require_decision: Option<Decision>,
/// Refuse a wvp-0.2 package about another contract, by digest
#[arg(long, value_name = "SHA-256", value_parser = digest)]
pub(crate) expect_contract: Option<Sha256Digest>,
/// With --require-decision: accept the decision under whatever contract
/// each wvp-0.2 package carries, unpinned
#[arg(
long,
requires = "require_decision",
conflicts_with = "expect_contract"
)]
pub(crate) any_contract: bool,
/// The packages to verify (default: every stored package)
pub(crate) packages: Vec<String>,
}
Expand Down Expand Up @@ -684,7 +697,8 @@ pub(crate) struct Verify {
#[arg(long, value_name = "VERSION", value_parser = protocol)]
pub(crate) protocol: Option<ProtocolVersion>,
/// Exit 0 only if this decision was reproduced: SUPPORTED, CONTRADICTED
/// or UNDETERMINED. Another reproduced decision exits 4
/// or UNDETERMINED. Another reproduced decision exits 4, and so does a
/// wvp-0.2 package whose contract you did not pin with --expect-contract
#[arg(long, value_name = "DECISION", value_parser = decision)]
pub(crate) require_decision: Option<Decision>,
/// A work record you hold, as a JSON file: refuse a package the record
Expand All @@ -697,6 +711,15 @@ pub(crate) struct Verify {
/// Refuse a package about another contract, by digest (wvp-0.2)
#[arg(long, value_name = "SHA-256", value_parser = digest)]
pub(crate) expect_contract: Option<Sha256Digest>,
/// With --require-decision: accept the decision under whatever contract
/// the package carries, unpinned (wvp-0.2). The package's producer chose
/// that contract, and one that requires nothing is SUPPORTED
#[arg(
long,
requires = "require_decision",
conflicts_with = "expect_contract"
)]
pub(crate) any_contract: bool,
/// Refuse a package carrying another claim, by identifier (wvp-0.2)
#[arg(long, value_name = "CLAIM")]
pub(crate) expect_claim: Option<String>,
Expand Down
3 changes: 2 additions & 1 deletion crates/verified-cli/src/exit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ pub(crate) enum Exit {
/// registry's log does not reach an entry held against it.
Incomplete = 3,
/// Every check ran and the package was not refused, but the decision it
/// reproduced is not the one `--require-decision` asked for.
/// reproduced is not the one `--require-decision` asked for, or it was
/// reproduced under a `wvp-0.2` contract the caller did not pin.
DecisionNotRequired = 4,
}

Expand Down
66 changes: 44 additions & 22 deletions crates/verified-cli/src/portable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ use std::path::Path;
use serde_json::json;
use verified_canonical::Limits;
use verified_protocol::Decision;
use verified_verifier::{DecisionFinding, Inspection, Options, Rejection, inspect, verify};
use verified_verifier::{
DecisionFinding, Expectations, Inspection, Options, Rejection, inspect, verify,
};
use verified_workspace::{Stored, Workspace};

use crate::cli::{Diff, Export, Replay};
Expand Down Expand Up @@ -318,7 +320,8 @@ pub(crate) fn diff(command: &Diff, out: &mut Output) -> Exit {

/// What verifying one stored package now concluded.
enum Replayed {
Reproduced(Decision),
/// The decision reproduced, and whether its contract was pinned.
Reproduced(Decision, bool),
Refused(String),
Damaged(String),
}
Expand All @@ -329,9 +332,9 @@ fn replay_one(workspace: &Workspace, stored: &Stored, options: &Options<'_>) ->
Err(error) => return Replayed::Damaged(error.to_string()),
};
match verify(&bytes, options) {
Ok(report) => match report.decision {
Ok(report) => match &report.decision {
DecisionFinding::Reproduced(reproduced) if report.is_complete() => {
Replayed::Reproduced(reproduced.decision)
Replayed::Reproduced(reproduced.decision, report.contract_pinned())
}
// A registry was supplied, so a report is complete; anything
// else is not something to call a success.
Expand All @@ -342,6 +345,27 @@ fn replay_one(workspace: &Workspace, stored: &Stored, options: &Options<'_>) ->
}
}

/// One package's line in `replay --json`.
fn replayed_json(stored: &Stored, result: &Replayed) -> serde_json::Value {
let (status, detail, pinned) = match result {
Replayed::Reproduced(decision, pinned) => {
("reproduced", decision.to_string(), Some(*pinned))
}
Replayed::Refused(code) => ("refused", code.clone(), None),
Replayed::Damaged(detail) => ("damaged", detail.clone(), None),
};
let mut entry = json!({
"package": stored.package.to_string(),
"object": stored.object.to_string(),
"status": status,
"detail": detail,
});
if let Some(pinned) = pinned {
entry["contractPinned"] = json!(pinned);
}
entry
}

pub(crate) fn replay(command: &Replay, out: &mut Output) -> Exit {
let limits = Limits::DEFAULT;
let trust = match registry(&command.trust, &limits) {
Expand All @@ -368,6 +392,10 @@ pub(crate) fn replay(command: &Replay, out: &mut Output) -> Exit {
let options = Options {
limits,
trust: Some(&trust),
expectations: Expectations {
contract: command.expect_contract,
..Expectations::default()
},
..Options::default()
};
let results: Vec<(Stored, Replayed)> = selected
Expand All @@ -377,40 +405,32 @@ pub(crate) fn replay(command: &Replay, out: &mut Output) -> Exit {

let failed_count = results
.iter()
.filter(|(_, r)| !matches!(r, Replayed::Reproduced(_)))
.filter(|(_, r)| !matches!(r, Replayed::Reproduced(..)))
.count();
let unmet = command.require_decision.map_or(0, |required| {
results
.iter()
.filter(|(_, r)| matches!(r, Replayed::Reproduced(d) if *d != required))
.filter(|(_, r)| {
matches!(r, Replayed::Reproduced(d, pinned)
if *d != required || !(*pinned || command.any_contract))
})
.count()
});
match out.mode {
Mode::Quiet => {}
Mode::Json => out.json(&json!({
"outcome": if failed_count > 0 { "failed" } else if unmet > 0 { "decision_not_required" } else { "replayed" },
"packages": results.iter().map(|(stored, result)| {
let (status, detail) = match result {
Replayed::Reproduced(decision) => ("reproduced", decision.to_string()),
Replayed::Refused(code) => ("refused", code.clone()),
Replayed::Damaged(detail) => ("damaged", detail.clone()),
};
json!({
"package": stored.package.to_string(),
"object": stored.object.to_string(),
"status": status,
"detail": detail,
})
}).collect::<Vec<_>>(),
"packages": results.iter().map(|(stored, result)| replayed_json(stored, result)).collect::<Vec<_>>(),
"requiredDecision": command.require_decision.map(Decision::as_str),
"note": "every result was computed in this run under the supplied registry; nothing was reused",
})),
Mode::Human => {
for (stored, result) in &results {
let id = format!("wvp:sha-256:{}", out.short(&stored.package.commitment().to_hex()));
let (tone, text) = match result {
Replayed::Reproduced(Decision::Supported) => (Tone::Established, "SUPPORTED reproduced".to_owned()),
Replayed::Reproduced(decision) => (Tone::Unknown, format!("{decision} reproduced")),
Replayed::Reproduced(Decision::Supported, true) => (Tone::Established, "SUPPORTED reproduced".to_owned()),
Replayed::Reproduced(decision, true) => (Tone::Unknown, format!("{decision} reproduced")),
Replayed::Reproduced(decision, false) => (Tone::Unknown, format!("{decision} reproduced, contract not pinned")),
Replayed::Refused(code) => (Tone::Negative, format!("refused: {code}")),
Replayed::Damaged(detail) => (Tone::Negative, format!("damaged: {detail}")),
};
Expand All @@ -423,7 +443,9 @@ pub(crate) fn replay(command: &Replay, out: &mut Output) -> Exit {
results.len()
));
if let Some(required) = command.require_decision {
out.line(&format!("required decision {required}: {unmet} package(s) reproduced another"));
out.line(&format!(
"required decision {required}: {unmet} package(s) reproduced another, or under a contract not pinned"
));
}
}
}
Expand Down
Loading
Loading