Skip to content

Milestone 1, slice 5: output - #15

Merged
davidchaiken merged 8 commits into
mainfrom
milestone-1
Oct 21, 2026
Merged

davidchaiken merged 8 commits into
mainfrom
milestone-1

Conversation

@davidchaiken

Copy link
Copy Markdown
Owner

Milestone 1, slice 5: output

This slice gives vcrd's output its three formats and its verbosity levels, makes masking visible value by value, and adds the levels of --unsafe. text becomes the default. A first draft (cf54833) was reviewed and revised in four rounds, one commit each, listed below.

Example of what a reader sees

text at the default level, for a credential that carries its own key and names no issuer:

vcrd verify fixtures/embedded-jwk-only.jwt --now 2026-10-01T00:00:00Z
 Status     inspect_failed
 Kind       credential
 Types      VerifiableCredential, ExampleDegreeCredential
 Validity   current

Phases
 Phase   | Outcome | Findings                    | Blocked by
---------+---------+-----------------------------+------------
 inspect | failed  | inspect.issuer_missing      |
 verify  | failed  | verify.embedded_key_refused |

Proof
 Suite                jws
 Algorithm            EdDSA
 Outcome              not_attempted
 Carried key          header.jwk
 Matched              false
 Verifies signature   true

Claims
 Path                          | Type   | Value    | Visibility
-------------------------------+--------+----------+------------
 credentialSubject.degree.name | string | (masked) | masked
 credentialSubject.degree.type | string | (masked) | masked
 credentialSubject.id          | string | (masked) | masked
 id                            | string | (masked) | masked

Findings
 Code                        | Attribution | Severity | Detail
-----------------------------+-------------+----------+----------------------
 inspect.issuer_missing      | input       | error    |
 verify.embedded_key_refused | policy      | error    | location: header.jwk

Not evaluated
 Check                | Reason
----------------------+-----------------
 issuer_accreditation | out_of_scope
 context_resolution   | not_implemented

Changes

Formats.

  • The three formats. --format text (the default, whether or not stdout is a terminal) prints labeled sections of tables. plain prints one name: value line per item, or name: column=value … for a table row. json prints the complete result as before.
  • One list of sections. text and plain draw on the same sections (vcrd-cli/src/text.rs), so the two always show the same information.
  • Escaping. In text and plain, every control character and every character that reorders bidirectional text is printed as an escape such as \u{001b}, so a credential's content can't act on the terminal or fake a line of output.
  • Wrapping. Prose wraps at 72 characters in text. The header's kid breaks before its #. Other values stay on one line, so they can be copied and searched for.
  • No color, so NO_COLOR holds trivially.

Verbosity. --verbosity 0–3 chooses how much text and plain show; JSON is always complete.

  • Level 0 prints nothing in any format.
  • Level 1 shows the status, the credential's kind, issuer, types and validity, the inspect and verify phases with what blocked them, each proof's outcome and key source, any carried key, the claims, the findings, and the checks not evaluated.
  • Level 2 adds everything except the credential's metadata, including the header.
  • Level 3 adds the metadata.

The status is always the first line of text and plain.

Masking, --unsafe, and visibility.

  • --unsafe has three levels:
    • 0 masks claims and cryptographic secrets;
    • 1 (--unsafe, --unsafe=1 or --unsafe 1) shows claims;
    • 2 also shows secrets, which in milestone 1 are a header jwk's private members.
  • Visibility. Every value carries a visibility of non_sensitive, masked or revealed: in JSON on each entry of credential.claims, credential.metadata and format.header, and as a Visibility column in text and plain.
  • reveals lists each revealed value with its path, the key the path is relative to, and visibility: revealed.
  • The warning. --unsafe writes a warning to stderr, and repeats it in the result: warnings in JSON, a Warnings table or line in text and plain.
  • In core, the format now classes each value as metadata, a claim, or a secret, and the header is rendered through the same masking as the claims.

The header. format.header lists every member of the JOSE header as values, as the claims are listed.

Keys.

  • Each thumbprint has its key beside it. key_provenance.jwk and credential_key.jwk hold each key as a JWK (JSON Web Key): the public members its RFC 7638 thumbprint covers. They come from a new core type, PublicJwk.
  • Findings about keys say where each key came from:
    • credential_key_mismatch is now {carried_key: {location, thumbprint}, key: {source, method, thumbprint}};
    • weak_key names its key the same way and adds criterion: small_order.

Exit codes. No output setting changes the exit code. --verbosity 0 --unsafe is accepted and prints nothing, where the draft treated it as a caller error.

Blocks.

  • A parse block is now {phase: parse, reason: impossible} in JSON, without missing: document, and reads "parse failed" in text and plain.
  • A block for want of a key keeps missing: key_material and reads "inspect: no key to verify with; looked in: issuer identifier".

Other.

  • Spelling. British spellings changed to American English (11 lines in 7 files).
  • [P7] resolved. Code-owner review is enforced, and GitHub exempts a PR's author. PR Bump the cargo group with 3 updates #14, Dependabot's Cargo.lock update, needed the code owner's review or the bypass.
  • deny.toml. The syn@2 exception now names only curve25519-dalek-derive, since PR Bump the cargo group with 3 updates #14 removed zerocopy.

The review rounds

Commit Notes What changed
f794a82 exit codes, stderr messages exit code independent of output settings; warnings; --unsafe 2; prose wrapping
f610f23 the Unmasked column, treatment, the masked header value visibility everywhere; the header as a list of values; reveals entries renamed to visibility
bc07066 "document missing" the parse block and the key-material block, reworded for the reader
8a2ed28 carried keys, thumbprints, the weak key jwk beside each thumbprint; key findings name their keys' sources; the carried key at level 1
04f5528 the reader's view the status first; the kid broken at #

Two notes moved to later work:

  • A user-facing reference for all 62 finding codes moves to slice 6, to be written with the milestone review.
  • Using a kid that is a did:key URL as a carried key, when the issuer gives none, is recorded in milestone 2's scope beside the embedded-key opt-in.

Dependencies

vcrd-cli adds tabled 0.22 with std only. It doesn't use the derive macro, since the columns change with the verbosity level, and leaves out color (ansi), macros and assert.

  • Cargo.lock gains six entries. Five are built: tabled, papergrid, bytecount, fnv and unicode-width, all MIT or Apache-2.0. testing_table is listed but never built.
  • cargo-deny passes.

Tests

  • Canary (vcrd-cli/tests/redaction.rs, with the new fixtures/canary.jwt):
    • Exact masking. Across three formats, three verbosity levels and three --unsafe levels, exactly the right canaries are printed, none reaches stderr, the warning is on both streams exactly when asked for, and the status is the first line.
    • Shown failing. It failed with claim masking broken in core, which was then restored.
    • Also covered: the shape of reveals, --verbosity 0 with --unsafe, and the warning after a caller error.
  • Exit codes (vcrd-cli/tests/exit_codes.rs): the example, every fixture and an unparseable stdin input, each at every format, verbosity level and --unsafe level. That's 720 runs in about 2 seconds, each required to give the input's one exit code, equal to the envelope's exit_code.
  • Expected output (vcrd-cli/tests/output.rs): 14 cases compared with files in vcrd-cli/tests/expected/, with an ignored test that regenerates them.
  • Unit tests: the escaping and plain quoting, the --unsafe space form, and the thumbprints from PublicJwk.
  • Existing tests: the JSON helper now asks for --format json, since text is the default.

Documents

  • docs/output.md: the formats, the verbosity table, --unsafe, values and their visibility, format.header, blocks in the reader's terms, keys and thumbprints, and text and plain examples. Its nine examples match the binary.
  • ARCHITECTURE:
    • §2: tabled;
    • §4: the CLI's steps;
    • §6: the envelope, including warnings;
    • §7: the three classes of value, and the canary test;
    • §8: the provenance example with both JWKs;
    • §10 [P7]: resolved.
  • REQUIREMENTS:
    • §8: the --unsafe levels, JSON always complete, and no output setting changing the exit code;
    • §16 item 20: reworded.
  • DEVELOPMENT-PLAN: milestone 1's output decisions (2026-10-08, with the dated revisions of 2026-10-21), and milestone 2's kid item.
  • README: the default output is now tables.

Checks

make ci-ok and make test-powerset pass locally.

🤖 Generated with Claude Code using Opus 5.5 - Extra effort

@davidchaiken

Copy link
Copy Markdown
Owner Author

The PR description describes the rounds of review that I did by looking at all of the permutations of output and the code.

@davidchaiken

Copy link
Copy Markdown
Owner Author

I also did a quick scan of all of the files in the PR in the GitHub "Files changed" tab.

@davidchaiken
davidchaiken merged commit 76c8071 into main Oct 21, 2026
9 checks passed
@davidchaiken
davidchaiken deleted the milestone-1 branch October 21, 2026 23:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant