Skip to content

contacts batch: no infrastructure as contacts (#775), code contacts complete 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

Merged
emooreatx merged 13 commits into
mainfrom
fix/contacts-batch-775-782
Oct 16, 2026

Conversation

@emooreatx

Copy link
Copy Markdown
Contributor

The server half of Eric's contacts report (2026-10-16). Each fix has a test that fails without it.

Issue Change Proof
#775 list_contacts skips canonical servers and the owner's own devices. The traces consent covers chat: by default, so it listed the canonical as a person. contacts_chat::a_node_that_consents_to_send_traces_lists_no_infrastructure_and_no_self_as_contacts; red with the skip disabled
#777 A v3 contact that answered 202 awaiting_key_body completes on its own when the key arrives: the same add path re-runs (pending_contacts, 10 s pass, 24 h TTL). native contact_edges: 202, then listed about 30 s later with no second paste, then the chat opens; red with the completer off, showing Eric's 403 not_a_contact
#778 (no product change needed once the node joins the mesh as an agent does) native contact_edges: 404 unknown_fed_id, then announce, then the same add succeeds in about 150 s and the chat opens
#776 Contact refusals log their operator detail under the reason_id. The text people see comes from CIRISClient's bundle by reason_id (Eric's "Admit the key first (peering)" is CIRISClient's en.json), and the localization guard pins the server's fallback to it, so the rewrite is proposed on CIRISClient#188. refusal_copy_tests::contact_refusals_speak_plainly_to_people; localization guard passes
#780 The refusal helpers log by class: 2xx is pending, 4xx refused, 5xx unavailable (WARN). answer_class_tests
#781 The scorer skips the agent this node hosts by rule, before scoring; it is counted and named, and accounts for a zero pass. capacity_scorer::the_agent_this_node_hosts_is_skipped_by_rule_not_erred; red with the skip disabled
#782 45 citations in server WARN/ERROR text now read history: X#N or tracked: X#N. tests/log_citations.rs (offline gate); tools/check_log_citations.py (state check: 45 checked, 0 mismatched). Edge half: CIRISEdge#875
#779 native fresh_claim guard It passes on both v0.5.223 and main: a clean fresh claim does not reproduce the storm. The real chain (a stranded owner key, so never Rooted) is proven in #784.

Local: contacts_chat 36/36, capacity_scorer 5/5, lib 632/632, localization guard green. The key fix that likely drives most of Eric's report is #784, which merges first.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WW3mUN8f2rUCYJVmyLwHGb

emooreatx and others added 8 commits October 16, 2026 11:04
… 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
… '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
…; 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
…ey/due types

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WW3mUN8f2rUCYJVmyLwHGb
…re 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
… 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
…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
… 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
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-10-16T17:06:38.205537Z 4cc3b8a PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

emooreatx and others added 2 commits October 16, 2026 12:53
…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
… 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
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 and others added 3 commits October 16, 2026 15:48
…5-782

# Conflicts:
#	harness/native/scenarios.py
…ain red on log_citations

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WW3mUN8f2rUCYJVmyLwHGb
…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
emooreatx merged commit 86003a8 into main Oct 16, 2026
16 checks passed
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>
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