Skip to content

Summarise every token with view --summary - #19

Draft
bajtos wants to merge 1 commit into
mainfrom
feat/view-summary
Draft

Summarise every token with view --summary#19
bajtos wants to merge 1 commit into
mainfrom
feat/view-summary

Conversation

@bajtos

@bajtos bajtos commented Aug 28, 2026

Copy link
Copy Markdown
Member

Written by Claude.

Reading the audience out of a pasted proof takes one ucantool view -i per entry today:

for i in 0 1 2 3 4; do
  ucantool view -i "$i" -j proof.txt | jq -r '.[1]["ucan/dlg@1.0.0-rc.1"].aud'
done

There is no way to learn the entry count except walking -i until it errors, every entry costs a process, and the jq path carries the spec version. A script comparing audiences against a configured DID breaks silently when UCAN moves past 1.0.0-rc.1, because jq -r on a missing key prints null, which a naive comparison reads as "audience does not match".

--summary decodes every token in the input and reports its fields:

$ ucantool view --summary proof.txt
+---+-----------------------+---------------------------+--------------------------+
| # |        COMMAND        |         AUDIENCE          |          ISSUER          |
+---+-----------------------+---------------------------+--------------------------+
| 0 | /s3/request/authorize | did:web:ingot.dev.example | did:web:hilt.dev.example |
+---+-----------------------+---------------------------+--------------------------+
$ ucantool view --summary -j proof.txt | jq -r '.[].aud'
did:web:ingot.dev.example

The JSON keys (index, tag, cmd, iss, aud, sub, exp) are fixed by this tool rather than borrowed from the encoded tag, so .[].aud keeps resolving across UCAN versions. The tag is reported as a value for callers that care. An entry that decodes as no known token kind reports its index and an error, leaving the other entries readable. A single delegation summarises the same way as a container.

-j without --summary is untouched: it still emits DAG-JSON of the container with entries as opaque bytes.

One behaviour change worth review

The container branch now reads its entries straight out of the input. It used to decode the container and re-encode it as raw to get the entry list, and that roundtrip sorts the entries bytewise and silently drops the ones decodeTokens cannot read, so indices and entry count could disagree with the file. The plain view table on a container therefore now shows the file's real entries in file order, and for a container whose entries were not already sorted, the displayed root CID changes. Containers this tool produces are already sorted, so nothing moves on the happy path.

Stripping the transport encoding (codec byte, base64, gzip) locally duplicates about twenty lines that container.Decode keeps unexported. We can remove that duplication after fil-forge/ucantone#55 is landed.

Tests

cmd/view_test.go drives rootCmd the way cmd/delegate_test.go does, building fixtures with ucantool delegate rather than committing any. It pins that every entry of a three-command container appears with its audience, that the JSON keys carry no spec version, that a bare delegation summarises, that an undecodable entry reports its index without hiding the others, and that -i <n> and --summary agree on the audience of entry n. Both paths go through one decodeToken helper, so they cannot drift on what counts as a delegation.

Reading the audience out of a pasted proof took one `ucantool view -i`
per entry, with no way to learn the entry count except walking the index
until it errored, and a jq path carrying the UCAN spec version. A script
comparing audiences against a configured DID broke silently when the
version moved, because `jq -r` on a missing key prints `null`.

`--summary` decodes every token in the input and reports its command,
audience and issuer as a table, or the full field set as JSON under keys
this tool fixes rather than borrowing from the encoded tag. An entry
that decodes as no known token kind reports its index and an error, so
the remaining entries stay readable. A single delegation summarises the
same way as a container.

The container branch now reads its entries straight out of the input.
It used to decode the container and re-encode it as raw, which sorts the
entries and drops the ones that decode as no known token kind, so
indices and entry count could disagree with the file. Stripping the
transport encoding locally duplicates about twenty lines that
`container.Decode` keeps unexported; the duplication goes away once
ucantone exports that step.

Assisted-by: Claude:claude-opus-5[1m]
Signed-off-by: Miroslav Bajtoš <oss@bajtos.net>
@bajtos
bajtos requested a review from alanshaw August 28, 2026 14:42
bajtos added a commit to fil-forge/ucantone that referenced this pull request Aug 28, 2026
_Written by Claude._

`container.Decode` is the only way to read a container, and it is lossy.
It returns a `*Container` of decoded tokens, so three properties of the
input are gone by the time it returns: `decodeTokens` skips any entry
that decodes as none of delegation, receipt or invocation, the tokens
are split into three slices by kind, and `encodeTokens` sorts bytewise
on the way back out, so a `Decode` then `Encode(Raw, …)` roundtrip does
not reproduce the input's entry order either.

A tool that inspects containers rather than executing them needs the
entries as they appear in the file: same order, same count, undecodable
ones included, so it can report an index that matches what a person or a
script counts. Today the only way to get them is to reimplement the
transport step outside the package, which duplicates about twenty lines
of `Decode` and will drift from it. `ucantool view --summary` carries
that copy now - see fil-forge/ucantool#19. I'd
like to rework that pull request to use the new API added by this
change.

## What this changes

`DecodeTransport(input []byte) (byte, []byte, error)` strips the
transport encoding and returns the codec byte and the CBOR of the
container model. The body of `Decode` moves into it unchanged up to the
`UnmarshalCBOR` call, error strings included, so what counts as a
decodable container cannot diverge between the two. `Decode` is now a
wrapper over it.

A caller reaches the raw entries through the already-exported datamodel:

```go
codec, cborBytes, err := container.DecodeTransport(input)
model := datamodel.ContainerModel{}
err = model.UnmarshalCBOR(bytes.NewReader(cborBytes))
// model.Ctn1 is the entries, in file order
```

Returning the codec byte saves the caller re-reading `input[0]`;
`FormatCodec` turns it into a name.

Nothing about `Decode`'s signature or behaviour changes, and the
existing `TestContainer` passes untouched. That is the evidence the
split is behaviour-preserving.

## Follow-up worth its own issue

`decodeTokens` swallowing undecodable entries and returning a nil error
is a real defect: a caller cannot tell a container of three tokens from
a container of three tokens plus two it could not read. Fixing it means
deciding whether `Decode` should fail or whether `Container` should
carry the unreadable entries, and that decision touches every consumer,
so it is left alone here.

## Testing

`GOWORK=off make ci` passes except `examples.TestServer`, which cannot
bind a TCP port in this sandbox.
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