Repository navigation
0.5.228: a re-open never supersedes a key — the owner's fed-ID opens from the tier its files are in; a stranded key is restored (#783) - #784
Merged
Conversation
…opens from the tier its files are in, and a key stranded by the keyring is restored (#783) Eric's desktop (agent 2.16.1 / server 0.5.227): every owner act fell to the machine path — "registered no user seed dir", "consent re-sign as owner failed: open the user-identity signing key (platform-sealed): Key not found: ed25519.seed", membership widener not installed, consent grants looping. Root cause, from his node log and the keyring source: his owner fed-ID mooreericnyc-ubnr7f7uxe was minted `platform-sealed` while the TPM was not usable, so ciris-keyring degraded to SOFTWARE storage ({alias}.master.key + {alias}.ed25519.seed.blob). On 2026-10-08 16:54:58Z the TPM plugin became loadable in-process (libciris_tpm_plugin.so ships in ciris_verify; the user is in tss). The next RE-OPEN (get_user_identity_signer → SealedEd25519Signer:: open_existing → create_platform_storage → PluginTpmSecureBlobStorage::new) found no seal, minted a fresh TPM master, witnessed THAT as "the higher-tier mint landed", and archived the live software key to *.superseded-pre-v8 — then loaded ed25519.seed from the empty TPM tier: KeyNotFound, forever. 19 s earlier the same key had signed his age record. Fix (server-side, no upstream wait): - src/key_tier.rs: the files decide the tier. A software master with no plugin seal opens as software, directly; a plugin seal opens through the factory (it unseals, never mints); only an alias with NO artifacts mints. Every sealed re-open and open-or-create in the server goes through it: the user Ed25519 re-open, all four ML-DSA-65 re-opens (the owner's PQC half was next in line — still software, one successful Ed25519 open away from the same supersession), the node/substrate open-or-create at every boot, and the legacy node probe. - The heal: an alias exactly as the bug leaves it — a plugin seal holding NO tpmp_blob beside a superseded software pair — is restored (archived files back under their names, the empty seal renamed *.orphaned-<unix>, nothing deleted, a loud WARN). The TPM tier held nothing, so the archive is the only key. - node_key::provision_node_identity (the embedded node's first birth, before compose) registers the owner seed dir, so the agent's pre-serve owner acts find the pen. Proof: tests/platform_sealed_reopen_keeps_the_key.rs, run with a real TPM — the server re-open keeps a software key and opens both halves with the same public keys; Eric's exact on-disk shape heals and opens the ORIGINAL keys; a TPM tier that holds a key is never overridden; a canary pins that the keyring itself still supersedes (it flips when upstream fixes). Operator probe on a COPY of Eric's files: healed, derived key id == mooreericnyc-ubnr7f7uxe, ML-DSA opened in place with nothing archived (copy deleted). Identity/key suites green: claim_remote, consent_survives_the_key_split, node_key_is_not_the_actor_key, owner_signer_capsule, portable_occurrence, self_identity_fold, the_sealed_half_ moves_into_this_home, split_node_authors_consent_as_itself, owner_is_the_identity. Opens 0.5.228 (Cargo.toml, Cargo.lock, evidence TSV version column). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WW3mUN8f2rUCYJVmyLwHGb
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WW3mUN8f2rUCYJVmyLwHGb
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
This was referenced Oct 16, 2026
…keys live there; the real-wire proof (#783) Building the regression scenario found the trap is wider than the owner key: CIRISEdge opens the node key at edge init and persist's keyring signer opens identity/keyring/ — both through create_platform_storage, both outside the server's routing. On v0.5.227, restarting a node after the TPM plugin becomes loadable logged SIX supersessions (the substrate key, the transport-identity keystore → 'generated fresh RNS transport identity', the keyring signer) and the node failed to boot. pin_process_tier(home): a home whose keys are all software-tier (a *.master.key, no *.tpmp_blob) gets CIRIS_TPM_PLUGIN pointed at a sentinel BEFORE any component opens a key, so no component can select the TPM tier and supersede them; a fresh home keeps TPM custody for new keys; a home with live TPM keys is untouched; a software→TPM move stays an explicit act. Called at serve start (cfg.home), at PyO3 import (CIRIS_HOME, else ~/ciris — the agent builds its Engine before any serve names a home) and at embedded provisioning. identity create's re-open goes through key_tier too (its PQC half is sealed in verify-core's create_federation_identity, which has the same hazard — noted on CIRISVerify#311). harness/native owner_key_tpm_appears (needs CIRIS_TPM_PLUGIN_REAL + a TPM): claim --backend platform-sealed with the plugin invisible (software tier), restart with it visible, the node opens the owner pen itself (membership widener installed) and nothing is archived; then Eric's exact shape heals and the pen works. This branch: PASS, 0 supersessions. v0.5.227: 6 supersessions, boot failure. Suites: key_tier 4/4, node_key_is_not_the_actor_key 19, claim_remote 5, owner_signer_capsule 6, portable_occurrence 2, self_identity_fold 14, the_sealed_half_moves_into_this_home 4, consent_survives_the_key_split 6; lib 630/630. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WW3mUN8f2rUCYJVmyLwHGb
…oid Keystore) goes to the keyring's own load-only open — macOS CI red PR #784's macos-latest: identity::tests mint_*_user_identity and minted_identity_composes_a_usable_claim_remote_signer failed 're-open sealed ML-DSA-65 half: Key not found: mldsa65.seed'. On macOS the keyring seals in the Secure Enclave / keychain tier, which writes neither a plugin seal nor a software master, so on_disk_tier read Absent and key_tier answered KeyNotFound itself. Absent now delegates to SealedEd25519Signer / SealedMlDsa65Signer:: open_existing (load-only, unchanged behaviour on those tiers); only a recognised SOFTWARE key bypasses the factory. identity::tests 6/6, key_tier 4/4 locally. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WW3mUN8f2rUCYJVmyLwHGb
…e — macOS CI red on #784 Without a TPM the canary asserted the keyring's OWN re-open of a software-tier key succeeds. On macOS the platform factory selects the keychain / Secure Enclave tier and cannot see software-tier files (KeyNotFound) — the same stranding by another route, which key_tier already handles (the_servers_reopen_keeps_a_ software_tier_key… covers the server path on every platform). The no-TPM branch now reports and returns; noted on CIRISVerify#311. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WW3mUN8f2rUCYJVmyLwHGb
emooreatx
added a commit
that referenced
this pull request
Oct 16, 2026
…784), the contacts batch (#785) Cargo.toml names the next tag (v0.5.227 is on 21869a6); the evidence rows follow it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WW3mUN8f2rUCYJVmyLwHGb
emooreatx
added a commit
that referenced
this pull request
Oct 16, 2026
…ain red on log_citations Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WW3mUN8f2rUCYJVmyLwHGb
emooreatx
added a commit
that referenced
this pull request
Oct 16, 2026
…e warnings cite it as tracked Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WW3mUN8f2rUCYJVmyLwHGb
emooreatx
added a commit
that referenced
this pull request
Oct 16, 2026
…omplete on their own (#777), plain refusals + pending logged as pending (#776, #780), scorer skips its own agent (#781), history-vs-tracked citations (#782), real-wire e2e (#777/#778/#779) (#785) * contacts: infrastructure and the owner's own devices are never listed as contacts (#775) Sharing traces with a canonical emits a consent:replication grant under the default prefixes, which include chat:, so every node that shares traces listed the canonical as a person (Eric's and Francesco's installs, 2026-10-16); a grant naming the owner's own client key or second node listed the owner as their own contact. list_contacts now skips a subject that is a canonical server (persist's is_canonical, or a directory row naming the canonical role) and a subject that resolves to the owner's fed-ID; the skip is logged at DEBUG. E2E: tests/contacts_chat.rs::a_node_that_consents_to_send_traces_lists_no_ infrastructure_and_no_self_as_contacts — default-prefix grants to the baked canonical and to the owner's identity list ZERO contacts; a person added beside them is listed alone. Proven red with the skips disabled. contacts_chat 36/36. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WW3mUN8f2rUCYJVmyLwHGb * refusal: log each typed answer by its class — a 202 is 'pending', not 'auth refused' (#780) refuse/refuse_with logged every answer as 'auth refused: …' at INFO, including the typed NOT-YET 202s (contacts.awaiting_key_body, awaiting_consent, unreachable); Eric and the agent session read the pending contact as a failure. Now the line says pending / refused / unavailable with the status, and a 5xx logs at WARN. Unit-tested classifier. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WW3mUN8f2rUCYJVmyLwHGb * contacts: a v3 contact completes on its own when the key arrives (#777); refusal operator detail moves to the log (#776, server half) #777 — POST /v1/contacts with a code whose key body this node lacks answered 202 contacts.awaiting_key_body with converges_on_its_own: true, yet nothing ever finished the contact: the person had to paste the code again. The add path is now add_contact_for_owner (the handler authorizes, then calls it), and pending_contacts holds the owner's intent (pasted code, owner authorization of that request) and re-runs the SAME path every 10 s once the key is in the directory: a match completes (grant emitted, chat opens), a refusal discards with the reason logged, 24 h expiry. In memory: a restart drops pending entries and the person can paste again. The e2e (two nodes, real wire) is the native harness scenario that follows. #776 — contact_stall_refusal now logs the operator detail (rungs, announce, admission, directory convergence) under the reason_id and answers the person in plain language where the server's text is what they see. The text a person actually reads comes from the CLIENT bundle by reason_id (Eric's 'Admit the key first (peering)… bookmark' is CIRISClient's en.json for contacts.unknown_fed_id), and the localization guard pins the server's fallback to that bundle for the literal ids, so those stay byte-identical until CIRISClient ships the rewrite (CIRISClient#188). Test: refusal_copy_tests::contact_refusals_speak_plainly_to_ people over every LadderStall. contacts_chat 36/36; localization guard passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WW3mUN8f2rUCYJVmyLwHGb * contacts: pending_contacts clippy — drop the unused len(), name the key/due types Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WW3mUN8f2rUCYJVmyLwHGb * scorer: a node never scores the agent it hosts — skipped by rule before the work, counted, named (#781) On an agent-hosted node the engine signs as the actor, so the hosted agent's traces carry a key this process wields. The scorer computed the score and only then hit CapacityAttestation::new's self-attestation guard, so every pass on Eric's desktop ended in 'capacity attestation violates CEG §7.5 anti-Goodhart'. Now an agent whose key is the attester or this process's actor identity is skipped BEFORE scoring (DEBUG line naming the rule anti_goodhart_self), counted as self_agents in every pass summary, and accounts for a zero pass instead of raising 'the agents do not account for it'. Test: capacity_scorer::the_agent_this_node_hosts_is_skipped_by_rule_not_erred — attester = the hosted agent's key: Ok(0), the guard is never reached, the rule is named, no zero-pass alarm. Proven red with the skip disabled. capacity_scorer 5/5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WW3mUN8f2rUCYJVmyLwHGb * logs: every issue cited in a WARN/ERROR says whether it is history or still tracked (#782) Eric and the agent session read steady-state warnings that cite issue numbers as live bugs; every one quoted had closed months earlier. In this crate's tracing::warn!/error! messages an issue citation now reads 'history: X#N' (closed — why the gate exists) or 'tracked: X#N' (open — work still owed): 45 distinct citations across 29 files, six of them tracked (CIRISServer#372, #601, #602, #630, #678, CIRISNodeCore#38). Gate (offline): tests/log_citations.rs fails on any bare citation in a warn!/error! string. State check (network, run before a cut): tools/check_log_citations.py — tracked must be OPEN, history CLOSED/MERGED; 45 checked, 0 mismatched. lib 632/632. The lines Eric actually saw repeating (item-2 drops, unattributed frames, CRPL source_key_id=None, no-route) are emitted by CIRISEdge — filed there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WW3mUN8f2rUCYJVmyLwHGb * harness: contact_edges — add by fed ID before and after convergence (#778), add by code before the key is held completes on its own (#777), on the real wire Native-harness scenario modelling production: each person's node joins the mesh the way an agent does (consent to replicate to the canonical — without it the first run reproduced Eric's symptom exactly: the key never reached the canonical, 'receive-axis pull found NO PEERS'). #778: an unknown fed ID is a 404 contacts.unknown_fed_id; after bob announces, the SAME add succeeds (150 s on this box), bob is listed, POST /v1/chat opens. #777: alice pastes carol's v3 code before holding carol's key → 202 contacts.awaiting_key_body → with no second paste carol is listed ~30 s later and the chat opens. PASS. Proven red with the pending completer disabled: the chat answers 403 chat.not_a_contact — Eric's symptom. python -m harness.native contact_edges --binary target/debug/ciris-server --owner-wait 240 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WW3mUN8f2rUCYJVmyLwHGb * harness: fresh_claim — a freshly claimed laptop node admits its owner rows at the canonical with no replication storm (#779 guard) Canonical + one laptop-class node claimed, announced and joined to the mesh as an agent joins (consent to the canonical). Asserts the canonical holds the owner's key and owner-signed rows within the window, and counts the storm signatures from Eric's 0.5.223 log (item-2 drops, source_key_id=None CRPL drops, round timeouts, occurrence refusals — raw lines plus log_dedup-folded repeats) against ceilings. Measured: PASS on main (owner rows in 52 s; 3/5/3/0) AND on a v0.5.223 test-anchor build (12 s; 3/4/3/0) — a clean fresh claim does not reproduce Eric's storm, which traces to his stranded owner key (#783: never Rooted). Kept as a guard; the regression for the real chain rides #784. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WW3mUN8f2rUCYJVmyLwHGb * logs: the no-analyze WARN reads 'WITHOUT the analyze grant (history: CC#46)'; its source-scan test keys on that stable phrase — ubuntu CI red on #785 The citation pass rewrote 'WITHOUT the CC#46 analyze grant' to 'WITHOUT the history: CC#46 analyze grant' — awkward mid-sentence, and fold_consent_surface::declining_to_be_scored_states_what_it_costs located the WARN by the old phrase. The citation moves to a parenthetical and the test finds 'WITHOUT the `analyze` grant'. fold_consent_surface 9/9, log_citations 1/1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WW3mUN8f2rUCYJVmyLwHGb * contacts: key_pull_unavailable/awaiting_key_body refusals back in the shape both localization scrapers read — ubuntu CI red on #785 localization_gate::rust_and_python_resolvers_agree_on_the_bundle: Rust saw 498 server ids, Python 499. The Rust scraper reads emitter-call argument positions and missed contacts.awaiting_key_body because a comment sat between the id and its message; the two key_pull_unavailable sites passed a block expression as the message, which hid their text from the server-id-text check. Comments move above the call, the operator detail is logged before refuse(), and the fallback text is the client bundle's verbatim (the rewrite is CIRISClient#188). localization_gate 9/9, guard --strict clean, contacts_chat 36/36. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WW3mUN8f2rUCYJVmyLwHGb * key_tier: the #783 warnings cite history (the fix landed in #784) — main red on log_citations Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WW3mUN8f2rUCYJVmyLwHGb * key_tier: #783 stays open (only its signing half landed in #784) — the warnings cite it as tracked Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WW3mUN8f2rUCYJVmyLwHGb --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
emooreatx
added a commit
that referenced
this pull request
Oct 16, 2026
…to peer (#177) (#786) * formal: AddDevice.tla — the add-device flow (device-join on the first-contact plane) model-checked with TLC Models a new device B joining as an occurrence of its owner's identity through the owner's existing device A, peer to peer on edge's first-contact opaque plane (CIRISEdge#683, kind 0x0000_0002), against an adversary with unlimited fresh keys that sees every request and injects any answer but cannot forge signatures. Five configurations: safety (8 invariants, 211,849 states, no error), careless owner (Inv_NoAttackerOwned violated — the receiver-derived matching code is load-bearing), liveness (holds), sybil flood without fair admission (liveness violated — availability-only residual), sybil with fair eviction (holds). Found: an answered request must free its pending slot, or a denied flood starves B. README records verdicts and the implementation requirements. For CIRISConstitution review (Eric's ask, 2026-10-16). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WW3mUN8f2rUCYJVmyLwHGb * formal/add_device README: what the model proves (Careful is an oracle, the code's load is the normative receiver derivation), owner presence named as the liveness assumption, a closed window refuses with no answer (CC review) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WW3mUN8f2rUCYJVmyLwHGb * add a device: a new device joins its owner's identity peer to peer, the owner consents on the existing device (CIRISServer#177) The first-run wizard's "Add device" on B takes the person's fedID (QR, long form or short form) and sends a device-join request over edge's first-contact opaque plane (CIRISEdge#683, kind 0x0000_0002), signed by B's node key. A holds it only while the owner has opened "Add a device"; both screens show a matching code A derives from (B's key, challenge). On approve, the owner pen on A signs owner -> B and B's occurrence (persist's content-only form), records both and re-wraps the self DEKs to B. B applies the binding through its own /v1/setup/root (its claim PIN still proves local presence) and stores the occurrence. B mints its own keys; no key material crosses. - FSD/ADD_DEVICE.md: flow, N1-N6, surfaces, refusal ids. - src/add_device.rs: wire types, A's answerer and owner routes, B's setup routes and poll; per-sender, ceiling and closed-window refusals; a decided request frees its slot at once (TLC finding, formal/add_device). - compose: edge gets the federation directory. First contact was dead in the server without it. - owner capsule: an active occurrence of the owner opens it, so B's self room and owner flows work on the occurrence key. - B's device keys use the default device custody (ML-DSA half sealed), so its pen opens through the normal signer path. - harness: `python -m harness.native add_device` passes end to end: closed window refused, two requesters with distinct codes, approve B, B applied and in session, occurrence on both nodes, A's self file opens on B, the stranger denied. - the eleven add_device.* ids are queued for the client bundle; ratchet 19 -> 30. approve is named in SESSION_CLAIMS §2.3 as a re-wrap caller. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WW3mUN8f2rUCYJVmyLwHGb * 0.5.228 opens: add a device, the key that stays where it was sealed (#784), the contacts batch (#785) Cargo.toml names the next tag (v0.5.227 is on 21869a6); the evidence rows follow it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WW3mUN8f2rUCYJVmyLwHGb * one_author_per_row: the add-device occurrence is a verify-produced signed occurrence at millisecond precision, like compose's Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WW3mUN8f2rUCYJVmyLwHGb --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the "cannot sign as the user" half of #783, found live on Eric's desktop (agent 2.16.1 / server 0.5.227).
Root cause
Eric's owner fed-ID
mooreericnyc-ubnr7f7uxewas mintedplatform-sealedwhile the TPM was not usable, so ciris-keyring stored it in its software tier. On 2026-10-08 16:54:58Z the TPM plugin became loadable in-process. The next re-open wentget_user_identity_signer → SealedEd25519Signer::open_existing → create_platform_storage → PluginTpmSecureBlobStorage::new, which minted a fresh TPM master, counted that as "the higher-tier mint landed", archived the live software key to*.superseded-pre-v8, and then loadeded25519.seedfrom the empty TPM tier:KeyNotFound. Every owner act since has fallen to the machine path, which is the consent loop, the missing membership widener and, very likely, the node never being Rooted.Fix
src/key_tier.rs: the files decide the tier. A software master with no plugin seal opens as software; a plugin seal opens through the factory, which unseals and never mints; only an alias with no artifacts mints. Every sealed re-open and open-or-create goes through it: the user Ed25519 key, all four ML-DSA-65 re-opens, the node and substrate keys at every boot, and the legacy node probe.*.orphaned-<unix>, nothing is deleted, and a loud WARN is logged.Proof
tests/platform_sealed_reopen_keeps_the_key.rs, run with a real TPM:An operator probe on a copy of Eric's files healed it and derived exactly
mooreericnyc-ubnr7f7uxe; the ML-DSA half opened in place with nothing archived; the copy was deleted. Nine identity and key suites are green locally. Upstream: CIRISVerify issue linked below.Opens 0.5.228. Runs under
ci:full.🤖 Generated with Claude Code
https://claude.ai/code/session_01WW3mUN8f2rUCYJVmyLwHGb