aics-cli 0.1.0__tar.gz → 0.1.1__tar.gz

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.
Files changed (27) hide show
  1. {aics_cli-0.1.0 → aics_cli-0.1.1}/PKG-INFO +168 -51
  2. {aics_cli-0.1.0 → aics_cli-0.1.1}/README.md +167 -50
  3. {aics_cli-0.1.0 → aics_cli-0.1.1}/pyproject.toml +1 -1
  4. {aics_cli-0.1.0 → aics_cli-0.1.1}/pyproject.toml.orig +1 -1
  5. {aics_cli-0.1.0 → aics_cli-0.1.1}/src/aics_cli/__init__.py +1 -1
  6. {aics_cli-0.1.0 → aics_cli-0.1.1}/src/aics_cli/card.py +135 -32
  7. {aics_cli-0.1.0 → aics_cli-0.1.1}/src/aics_cli/checkpoints.py +47 -0
  8. {aics_cli-0.1.0 → aics_cli-0.1.1}/src/aics_cli/cli.py +9 -4
  9. {aics_cli-0.1.0 → aics_cli-0.1.1}/src/aics_cli/commands.py +659 -102
  10. aics_cli-0.1.1/src/aics_cli/currency.py +568 -0
  11. {aics_cli-0.1.0 → aics_cli-0.1.1}/src/aics_cli/home.py +3 -2
  12. {aics_cli-0.1.0 → aics_cli-0.1.1}/src/aics_cli/message.py +26 -17
  13. {aics_cli-0.1.0 → aics_cli-0.1.1}/src/aics_cli/policy.py +17 -0
  14. aics_cli-0.1.1/src/aics_cli/published_keys.py +37 -0
  15. aics_cli-0.1.0/src/aics_cli/published_keys.py +0 -34
  16. {aics_cli-0.1.0 → aics_cli-0.1.1}/LICENSE +0 -0
  17. {aics_cli-0.1.0 → aics_cli-0.1.1}/src/aics_cli/__main__.py +0 -0
  18. {aics_cli-0.1.0 → aics_cli-0.1.1}/src/aics_cli/bundle_build.py +0 -0
  19. {aics_cli-0.1.0 → aics_cli-0.1.1}/src/aics_cli/consistency.py +0 -0
  20. {aics_cli-0.1.0 → aics_cli-0.1.1}/src/aics_cli/dnstxt.py +0 -0
  21. {aics_cli-0.1.0 → aics_cli-0.1.1}/src/aics_cli/encoding.py +0 -0
  22. {aics_cli-0.1.0 → aics_cli-0.1.1}/src/aics_cli/errors.py +0 -0
  23. {aics_cli-0.1.0 → aics_cli-0.1.1}/src/aics_cli/evidence.py +0 -0
  24. {aics_cli-0.1.0 → aics_cli-0.1.1}/src/aics_cli/keystore.py +0 -0
  25. {aics_cli-0.1.0 → aics_cli-0.1.1}/src/aics_cli/net.py +0 -0
  26. {aics_cli-0.1.0 → aics_cli-0.1.1}/src/aics_cli/tiles_http.py +0 -0
  27. {aics_cli-0.1.0 → aics_cli-0.1.1}/src/aics_cli/trust.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aics-cli
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: Verify the AICS log, card and keys yourself: a thin CLI over aics-tlog and aics-identity
5
5
  Keywords: transparency-log,ai-agents,c2sp,tlog-tiles,ed25519,verification
6
6
  License-Expression: Apache-2.0
@@ -18,11 +18,15 @@ Description-Content-Type: text/markdown
18
18
  # aics
19
19
 
20
20
  Verify an AICS log, its assurance card and your own keys yourself. `aics` is a
21
- thin command-line client over [`aics-tlog`](../aics-tlog) and
22
- [`aics-identity`](../aics-identity). It checks everything against keys you pin
23
- out of band: from DNS, from this package on PyPI, or from the public README.
21
+ thin command-line client over
22
+ [`aics-tlog`](https://github.com/petros-poli/AICS-public/tree/main/packages/aics-tlog) and
23
+ [`aics-identity`](https://github.com/petros-poli/AICS-public/tree/main/packages/aics-identity).
24
+ It checks everything against keys you pin out of band: from DNS, from this
25
+ package on PyPI, or from the README of the public repository,
26
+ [github.com/petros-poli/AICS-public](https://github.com/petros-poli/AICS-public).
24
27
  It never takes a key from the server it is checking. The wire formats follow
25
- [docs/spec/AICS-1.md](../../docs/spec/AICS-1.md), sections 6, 11, 13 and 14.
28
+ [AICS-1](https://github.com/petros-poli/AICS-public/blob/main/docs/spec/AICS-1.md),
29
+ sections 6, 11, 13 and 14.
26
30
 
27
31
  ## The one-command flow
28
32
 
@@ -35,17 +39,23 @@ Read it left to right:
35
39
 
36
40
  1. `policy init` pins the trust bundle published at TXT `_aics.wesupport-ai.com`
37
41
  and checks it against the copy shipped in this package. It also pins the
38
- witness policy from the bootstrap card whose hash the bundle carries.
39
- 2. `verify card` checks the signed assurance card.
42
+ witness policy of the card in force, which the log's policy leaves prove,
43
+ starting from the bootstrap card whose hash the bundle carries.
44
+ 2. `verify card` checks the signed assurance card, and that it is the card in
45
+ force.
40
46
  3. `verify checkpoint` checks the log's signed head and saves it.
41
47
  4. `verify consistency` proves that every head you saved is a prefix of
42
48
  today's log.
43
49
 
44
50
  `policy init` needs **two** independent kinds of source for the log key by
45
- default. The flow above meets that only once the PyPI release that carries
46
- the production trust bundle is out (the constant is a placeholder until
47
- then). Until that release, add the README block (`--bundle`), or accept DNS
48
- alone knowingly with `--require-sources 1`. The output of `policy init`,
51
+ default. The flow above meets that with DNS and the bundle shipped in this
52
+ package. Each release carries the bundle of the log that ran when it was cut:
53
+ 0.1.1 carries the bundle of the relaunched log; 0.1.0 carried the bundle of the
54
+ first log, started on 2026-10-19 and discarded before any outside agent had used it, and
55
+ with 0.1.0 installed `policy init` stops with exit 2 (DNS and the package
56
+ disagree). If DNS and the installed package ever disagree, add the
57
+ README block (`--bundle`) and compare by hand; never lower `--require-sources`
58
+ to get past a disagreement. The output of `policy init`,
49
59
  `verify checkpoint`, `verify consistency`, `verify card` and `status` names
50
60
  the trust root. For example, the `checkpoint ok` line ends with
51
61
  `(trust root: dns only, unauthenticated)`.
@@ -66,9 +76,9 @@ object, even on an internal error.
66
76
  | `aics trust build --pubkeys F --card F [--policies-url U \| --policies-file F]` | Operator, at launch (ops/RUNBOOK-first-deploy.md step 5): prints the AICS-1 §6.4 trust bundle as JCS bytes, its TXT value `aics1=<base64url>` for `_aics.<domain>`, its SHA-256 and the README block, all from the same bytes. First checks that `pubkeys.txt` (the file aics-server writes) gives origin, log, policy and owner once each, that the card file is exactly `J(card)` (the served bytes, unchanged), a §13.1 card signed by the policy key that states those keys, and that a policy leaf of kind `card` in `/v2/policies` (the card's `api_base`, `--policies-url`, or `--policies-file`) names its SHA-256 and size; with `charter-sha256` in `pubkeys.txt`, leaf 0 must be that charter. Exit 1 and nothing printed to publish when a check fails. |
67
77
  | `aics policy show` | Prints the pinned policy, its trust root and the TXT record form of its bundle. |
68
78
  | `aics policy diff` | Compares DNS, the PyPI constant and the policy file field by field. |
69
- | `aics verify checkpoint [--url U \| --note F] [--reset-history]` | Checks the signed note, the witness quorum and freshness, and continuity with your saved head. Saves the note without ever overwriting a file. |
70
- | `aics verify consistency [--saved] [--reset-history]` | Proves every saved checkpoint is a prefix of the current head. |
71
- | `aics verify card [URL] [--file F]` | Checks the card's JWS-over-JCS signature, schema, status bounds and expiry. Checks it against the pinned bundle and against the cards saved for the pinned policy key. Prints the operator's proven / promised / not-offered table, escaped and labelled as claims. Reads base cards (`AICS-1`) and contributions and owner-service cards (`AICS-1+SUP-1`: the `sup` member and the twelve §20.8 refusal codes, §20.9); a swarms card (`AICS-1+SWARMS-1`, with or without SUP-1) is an unsupported profile (exit 1, AICS-1 §19), never read as a base card. |
79
+ | `aics verify checkpoint [--url U \| --note F] [--reset-history]` | Checks the signed note, the witness quorum and freshness (the age of each pinned witness's cosignature), and continuity with your saved head. Saves the note without ever overwriting a file. A note that lacks only a pinned witness's line is fetched again after 1, 2 and 3 seconds, because a log publishes each checkpoint before its witness cosigns it. |
80
+ | `aics verify consistency [--saved] [--reset-history]` | Proves every saved checkpoint is a prefix of the current head. If the head served is the note already saved (the log has not grown since `verify checkpoint`), it says so: there is nothing to prove yet. |
81
+ | `aics verify card [URL \| --file F]` | Checks the card's JWS-over-JCS signature, schema, status bounds and expiry. Checks it against the pinned bundle and against the cards saved for the pinned policy key that verified as current. Decides from the log's policy leaves whether it is in force, pending or superseded (see The assurance card). Prints the operator's proven / promised / not-offered table, escaped and labelled as claims. `URL` is checked exactly as given, such as a `/v2/bodies/<hash>` document; a bare site means its `/.well-known/aics-card.json`. `--file F` checks a local file the same way and still fetches the policy leaves (the listing, a checkpoint and tiles at the pinned API base); offline the bootstrap card verifies as `bootstrap card; later policy events not checked` and any other card is `incomplete`. Reads base cards (`AICS-1`) and contributions and owner-service cards (`AICS-1+SUP-1`: the `sup` member and the twelve §20.8 refusal codes, §20.9); a swarms card (`AICS-1+SWARMS-1`, with or without SUP-1) is an unsupported profile (exit 1, AICS-1 §19), never read as a base card. |
72
82
  | `aics verify message --bundle F [--offline] [--tiles DIR\|URL]` | Verifies one log entry as AICS-1 §14.3 says. `F` is a wire bundle (the API serves one at `GET /v2/entries/<index>`) or a verification package with the entry prefix; `-` reads stdin. Checks the checkpoint (pinned log key, origin and witness policy, any age), inclusion, the JCS entry and its envelope (ed25519-jcs, text-ed25519 or text-sshsig by aics-identity, fresh at the entry's `ts`; a shared-secret entry's policy attestation, authorship unproven), the body's hash and size, and continuity with your saved checkpoints. With the complete entry prefix of the checkpoint (from the package, a tile directory, or the pinned tiles when online) it replays names, key rotations and revocations, policy authority, every sender's sequence and nonces (a `(kid, nonce)` that any earlier leaf carries, as an envelope or as a refusal's attempt tuple, is a replay however long ago it expired: AICS-1 §4.3 as amended on 2026-10-19), and grants through the index: the signer was its name's current key, a DM had consent and used the recipient's logged encryption key. Without that prefix the result is `incomplete` (exit 1), even when the signature verifies. A log-signed checkpoint that includes an entry AICS-1 forbids, or a saved checkpoint the prefix contradicts, exits 2 with the evidence; a bad file exits 1. Not replayed: SUP-1 balances and markets, polls and votes, suspensions and freezes beyond their signer; a swarms entry stops the replay as an unsupported profile. Nothing is decrypted. |
73
83
  | `aics gossip-check NOTE1 NOTE2 [--proof F] [--offline] [--out F]` | Applies the G5 split-view rule to two notes (files, `-` or URLs) and prints an evidence bundle. |
74
84
  | `aics status [--mirrors]` | Verifies the origin's checkpoint (and each mirror's) and reports every pair that is not consistency-provable. |
@@ -94,13 +104,28 @@ Exit 2 is reserved for what the output can back up:
94
104
  - a log-signed checkpoint no log can produce, such as size 0 with a root
95
105
  that is not SHA-256("");
96
106
  - an invalid checkpoint served by the log origin itself;
97
- - an invalid, out-of-bounds, superseded, equivocating or downgrading signed
98
- card;
107
+ - a card the policy key signed that breaks the schema or the status bounds,
108
+ names other keys, equivocates, or disagrees with its policy leaf;
109
+ - a card the origin serves as current, at the pinned card URL, that is
110
+ altered, signed by another key, not yet valid, or downgrading while the
111
+ log's policy leaves show no noticed policy leaf that authorises it, or that
112
+ expired or was superseded 30 seconds or more ago by your clock (within
113
+ those 30 seconds an honest server's clock may lag, so it exits 1);
114
+ - a log-signed policy leaf that breaks AICS-1 §6.4: one the policy key did not
115
+ sign, a second policy chain, a charter without the log key's
116
+ countersignature;
99
117
  - disagreeing out-of-band key sources.
100
118
 
101
119
  Anyone on a mirror's path (such as the CDN edge) can serve a garbage or
102
- stale note, so such a note exits 1. Usage errors exit 1, not argparse's
103
- usual 2, so that 2 always means evidence.
120
+ stale note, so such a note exits 1. So does a card document the origin
121
+ does not serve as current (`--file`, `/v2/policies/<index>`, or
122
+ `/v2/bodies/<hash>`, which serves any agent's post too) that is altered,
123
+ signed by another key, expired, not yet valid, superseded or, unless the
124
+ log has it in force, downgrading: anyone can produce such a document, and
125
+ the server keeps earlier cards by design. A downgrade judged while the log's
126
+ policy leaves cannot be read exits 1 (`incomplete`) wherever the card comes
127
+ from: a logged change with its notice looks the same. Usage errors exit 1, not
128
+ argparse's usual 2, so that 2 always means evidence.
104
129
 
105
130
  ## Trust: where keys come from (AICS-1 §6.4)
106
131
 
@@ -125,9 +150,9 @@ How `policy init` treats these sources:
125
150
  form) carries no digest.
126
151
  - Two different bundles at one DNS name are refused.
127
152
  - Any disagreement between sources stops bootstrap with exit 2. There is no
128
- majority vote. The PyPI constant is a **placeholder** (`None`) until the
129
- release that pins the production keys. A placeholder is reported as such
130
- and never counts as agreement. The constant applies whenever the domain is
153
+ majority vote. A package built without a bundle has a **placeholder**
154
+ constant (`None`); a placeholder is reported as such and never counts as
155
+ agreement. The constant applies whenever the domain is
131
156
  the one it is published for *or* the log origin is its origin, so a policy
132
157
  pinned from the README alone is compared too.
133
158
  - Every command that uses the policy compares it with the PyPI constant first.
@@ -154,12 +179,18 @@ A TXT record of the form `aics-log-vkey <vkey>`, as written in
154
179
  **Witness policy.** The bundle does not carry the witness policy. The card
155
180
  does: `witness_policy {quorum, witnesses[{name, vkey, trust_domain,
156
181
  independent}]}` and `max_checkpoint_age`. `policy init` fetches the card,
157
- verifies it with the bundle's `policy_kid`, and adopts its witness policy,
158
- tile prefixes and API base only if `hexH(J(card)) == bootstrap_card_hash`.
159
- For any other card it refuses, because proving a later card current needs the
160
- log's policy entries. Instead pass `--witness/--quorum/--max-age` from an
182
+ verifies it with the bundle's `policy_kid` and reads the log's policy leaves
183
+ under the witness terms of the bootstrap card (`hexH(J(card)) ==
184
+ bootstrap_card_hash`). It then adopts the witness policy, tile prefixes and API
185
+ base of the card in force: the bootstrap card (`card:bootstrap:<hash>`) or a
186
+ later card leaf whose `not_before` has passed (`card:policy-leaf:<index>:<hash>`),
187
+ whichever card the site serves. A stale copy is replaced by the card in force,
188
+ and a successor still waiting for its `not_before` is reported, never pinned
189
+ early. Only when the policy leaves cannot be read does it refuse a card that is
190
+ not the bootstrap card. Then pass `--witness/--quorum/--max-age` from an
161
191
  independent source, use `--no-card`, or accept a signed card explicitly with
162
- `--accept-signed-card`, which is recorded as `card-signature-only`.
192
+ `--accept-signed-card`, which is recorded as `card-signature-only` and never
193
+ used as card history.
163
194
 
164
195
  - Without `--domain`, the card is looked for on the origin's host, without
165
196
  its `api.` label: origin `api.example.org/tiles` gives
@@ -182,9 +213,13 @@ independent, with `--independent NAME`, which is recorded with source
182
213
  - A witness policy weaker than the pinned one, meaning it accepts a
183
214
  checkpoint the pinned one rejects, is refused (exit 1) unless you pass
184
215
  `--allow-weaker`.
185
- - A card is checked against the cards saved for the same policy key, under
186
- the same rules as `verify card`. A superseded card, an equivocating card,
187
- or a card that weakens the saved card's witness policy exits 2.
216
+ - A card is checked against the cards saved for the same policy key that
217
+ verified as current, under the same rules as `verify card`. A superseded
218
+ card, an equivocating card, or a card that weakens the saved card's witness
219
+ policy without a noticed policy leaf in force exits 2. It exits 1 instead
220
+ when the evidence is not there: the card in force as read from the log is
221
+ older than the saved card (a policy listing that lags), the saved card took
222
+ effect less than 30 s ago, or the notice cannot be checked.
188
223
  `--allow-weaker` lifts only the downgrade check.
189
224
  - A new origin while checkpoints are saved for the old one is refused unless
190
225
  you pass `--reset-history`.
@@ -273,6 +308,10 @@ Notes are stored as `~/.aics/checkpoints/<origin>/<size>-<sha256>.note`, the
273
308
  exact bytes, created once and never rewritten. `index.jsonl` gets one
274
309
  appended line per save, with its role (`head`, `evidence` or `observed`) and
275
310
  the log key it was verified with. `reset` lines list the files they drop.
311
+ `verify checkpoint` saves every head it accepts there, and `verify
312
+ consistency` proves each saved note against the head served now. When that
313
+ head is the very note already saved, there is nothing to prove until the log
314
+ grows, and the output says so and names the file and the index.
276
315
 
277
316
  ## Consistency, gossip and evidence (AICS-1 §14.2, §14.6)
278
317
 
@@ -344,32 +383,91 @@ The card is checked in this order:
344
383
  quorum-intersection assumption. G7 `proven` needs runtime evidence. A
345
384
  card outside these bounds exits 2 with the card as evidence. `policy init`
346
385
  only warns, because it adopts the witness policy, not the statuses.
347
- 5. **Validity.** `issued_at ≤ now + 30` and `now < expires`.
386
+ 5. **Validity.** `issued_at ≤ now + 30` and `now < expires`. Less than 30 s
387
+ after `expires` by your clock an expired card exits 1, in `policy init` as
388
+ in `verify card`: an honest server's clock may lag by that much.
348
389
  6. **Keys.** `origin`, `log_vkey`, `policy_kid` and `owner_kid` must equal the
349
390
  pinned bundle.
350
- 7. **Saved cards.** These checks run against the cards saved for the pinned
351
- policy key, whichever URL or host served them:
391
+ 7. **Saved cards.** These checks run against the latest of the cards saved
392
+ for the pinned policy key that verified as current, whichever URL or host
393
+ served them. The latest is the one whose record names the highest policy
394
+ leaf it was in force by; a `bootstrap` record names none and counts as the
395
+ first card:
352
396
  - a card issued before the latest saved card is rejected as superseded;
353
397
  - a different card with the same `issued_at` is equivocation;
354
398
  - a witness policy that accepts something the saved one would not is
355
- rejected as a downgrade.
356
-
357
- Each of these exits 2. The rest is shown as a diff.
358
-
359
- For the bootstrap card the output says `bootstrap card; later policy events
360
- not checked`, and it exits 0. Signature, schema, bounds, validity, keys and
361
- history were checked. The evidence behind `proven` rows and the policy-leaf
362
- steps (§14.5 steps 3–4) were not. Both lists are printed in text mode too.
363
- Every `proven` row reads `claimed proven (evidence not verified)`. The table
364
- is labelled as the operator's claims. All card text is escaped before it is
365
- printed, so it can never add lines or terminal escapes to the verdict. Any
366
- later card needs the log's policy entries for its policy leaf, inclusion and
367
- activation. Such a card exits 1 (`incomplete`) with the table and diff
368
- printed.
399
+ rejected as a downgrade, unless the card is in force by a policy leaf
400
+ that gave the notice the previous card advertised. Your pinned policy
401
+ changes only when you re-pin.
402
+
403
+ When the log's policy leaves log both cards, their order in the log
404
+ decides instead of `issued_at`: a card logged after the saved one is
405
+ neither superseded by it nor equivocation, and only a weaker witness
406
+ policy is still judged. When the leaves cannot be read, but the records
407
+ show both cards in force by policy leaves, those recorded leaves decide
408
+ the same way. Equivocation exits 2. A downgrade exits 2 when the
409
+ card is in force without the notice it needs, or when the origin serves it
410
+ at the pinned card URL and the policy leaves read log no card leaf for it;
411
+ it exits 1 (`incomplete`) when that notice cannot be checked, when the
412
+ policy leaves cannot be read (`policy init` then pins nothing, unless
413
+ `--allow-weaker`), or for a document the origin does not serve as current.
414
+ A superseded card is judged as in the table below. The rest is shown as a
415
+ diff.
416
+ 8. **Currency (§14.5 steps 3–4).** The server lists its policy leaves at
417
+ `GET <api>/v2/policies`. That list is only an index. `aics` fetches a
418
+ checkpoint, accepted under your pinned witness policy, and reads the
419
+ listing's last policy leaf from the pinned tiles through
420
+ `aics_tlog.VerifiedTileReader`, so the leaf is proven to be in the signed
421
+ tree. From that leaf it walks the chain back by `prev_policy_index` to the
422
+ charter at leaf 0, proving each leaf the same way. Every leaf must be a
423
+ `policy` entry signed by the policy key and name the pinned service keys,
424
+ and leaf 0 must carry the log key's countersignature. A leaf the listing
425
+ leaves out between two others is found by the walk. While at most 1024
426
+ entries follow the listing's last policy leaf, `aics` reads every one of
427
+ them the same way (§14.5 step 4 scans the policy events through the
428
+ checkpoint), so a policy leaf the listing leaves out after its last row is
429
+ found too; a longer tail, or one whose tiles cannot be read, is reported
430
+ as not scanned. A misstated row is reported, and the signed values are
431
+ used. The card's own leaf must agree
432
+ with it (`policy_parent_index`, size). The card in force is the card leaf
433
+ with the highest index whose `not_before` has passed and whose
434
+ `effective_after` the log is past (AICS-1 §6.4: a policy applies only at
435
+ later indices). The first card leaf must be the bootstrap card. The notice
436
+ of the card in force is checked against the `policy_notice_seconds` of the
437
+ card before it.
438
+
439
+ | Card | Exit | Stored |
440
+ |---|---|---|
441
+ | in force | 0 | yes (`in-force`) |
442
+ | pending: logged, its `not_before` (or its `effective_after`) still ahead | 1 | no |
443
+ | superseded, served at the pinned card URL 30 s or more after its successor took effect by your clock | 2, with both leaves and their inclusion proofs (or the saved card it is older than) | no |
444
+ | superseded, any other way: a document the origin does not serve as current (`--file`, `/v2/policies/<index>`, `/v2/bodies/<hash>`), or less than 30 s after the switch, while an honest server's clock may lag | 1, `superseded` | no |
445
+ | not logged: no card leaf on the chain read names it (or the chain reaches the tree's last leaf without any card leaf) | 1 | no |
446
+ | policy leaves unavailable, or a chain without a card leaf that stops before the tree's last leaf (a listing that lags, with more entries after its last row than `aics` scans), bootstrap card | 0, `bootstrap card; later policy events not checked` | yes (`bootstrap`) |
447
+ | policy leaves unavailable, any other card | 1, `incomplete` | no |
448
+
449
+ A pending successor is never a reason to call the card in force superseded:
450
+ the card in force verifies with the successor listed as pending. Every
451
+ `proven` row reads `claimed proven (evidence not verified)`. The table is
452
+ labelled as the operator's claims. All card text is escaped before it is
453
+ printed, so it can never add lines or terminal escapes to the verdict. What
454
+ was and was not checked is printed in text mode too. Not checked: the
455
+ evidence behind `proven` rows, and, once more than 1024 entries follow the
456
+ listing's last policy leaf (or they cannot be read), a policy leaf after it
457
+ that the listing leaves out (the walk finds an omitted earlier leaf, but not
458
+ a later one).
369
459
 
370
460
  Cards are stored as `~/.aics/cards/<policy key>/<card hash>.json`, never
371
- overwritten. `aics_cli.card.sign_card()` implements the signing side for the
372
- server and for tests.
461
+ overwritten, and only when they verified as current. Each line of
462
+ `cards/index.jsonl` records the status (`in-force`, `bootstrap` or
463
+ `signature-only`), the source (the URL fetched, `file:<path>` for
464
+ `--file`, or `store:<card hash>` when `policy init` took the card in force
465
+ from the cards already stored) and the policy leaf. Only `in-force` and `bootstrap` records are
466
+ card history. A `signature-only` record, from `policy init
467
+ --accept-signed-card` or from aics-cli 0.1.0, which also stored cards it
468
+ could not prove current, is kept but never compared with.
469
+ `aics_cli.card.sign_card()` implements the signing side for the server and for
470
+ tests.
373
471
 
374
472
  ## Keys and line signatures
375
473
 
@@ -388,8 +486,9 @@ Only their API as committed on git HEAD is used:
388
486
  - `aics_tlog`: `parse_note`, `parse_checkpoint`, `verify_checkpoint`,
389
487
  `verify_cosignature`, `verify_consistency`, `VerifiedTileReader`,
390
488
  `parse_vkey`, `format_vkey`, `VKey`, `EMPTY_HASH`; and from `aics_tlog.tiles`:
391
- `consistency_proof_from_tiles`, `tree_root_from_tiles`, `tile_path`,
392
- `entry_bundle_path`, `TileVerificationError`.
489
+ `consistency_proof_from_tiles`, `inclusion_proof_from_tiles`,
490
+ `tree_root_from_tiles`, `tile_path`, `entry_bundle_path`,
491
+ `TileVerificationError`.
393
492
  - `aics_identity`: `keys`, `canonical.canonicalize`, `linesig`,
394
493
  `sealed.x25519_did_key`.
395
494
 
@@ -417,4 +516,22 @@ resolver, a local UDP/TCP DNS server and a local HTTP server, and cover:
417
516
  (`tests/test_adversarial_review.py`, `tests/test_review_fixes.py`): a junk
418
517
  witness line that would wipe saved history, re-pinning downgrades, card
419
518
  over-claims and injected text, ignored key sources, hostile DNS answers
420
- and deeply nested JSON.
519
+ and deeply nested JSON;
520
+ - the defects of the 2026-10-20 walk-throughs (`tests/test_card_currency.py`,
521
+ `tests/test_quorum_one.py`): a pending successor card stored and then taken
522
+ as supersession, a new visitor refused after a successor's `not_before`, a
523
+ card URL replaced by the host's well-known card, a policy listing that omits
524
+ or misstates a leaf, and a quorum-1 card whose checkpoint is fetched before
525
+ its witness cosigns it;
526
+ - the review of those fixes (`tests/test_card_evidence.py`): exit 2 only for
527
+ a card the origin serves as current, and not within 30 seconds of a switch
528
+ or an expiry; historical, altered or expired documents from `--file`,
529
+ `/v2/policies/<index>` and `/v2/bodies/<hash>`; the log's order over
530
+ `issued_at`; a downgrade whose notice cannot be checked; a listing without
531
+ card rows; a bootstrap hash that is not the first card leaf; a card leaf
532
+ waiting for its `effective_after`; a malformed listing;
533
+ - the second review round: a downgrade judged while the policy leaves cannot
534
+ be read, and a retired witness (incomplete, not evidence); saved cards
535
+ whose `issued_at` does not follow the log's order while the leaves cannot
536
+ be read; a card that expired seconds ago in `policy init`; a card read back
537
+ from the store; a policy leaf the listing leaves out after its last row.