RuCelium: federated environmental intelligence fabric (ADR-264…269) — runtime, hardening, applications - #2
Conversation
- docs/ADR-264-rumycelium-federated-fabric.md: federated environmental intelligence fabric spec (four layers, data economics, governance, acceptance criteria) - rumycelium-core: EnvSample (twelve mandatory attributes), EnvFrame, CalibrationRecord (Q16.16 lineage-chained), EnvironmentalEvent, SensorModality registry, GeoPoint with exact privacy coarsening, three-tier DataClass residency model - rumycelium-abi: rv_env_sample_v1 packed LE wire format with bounds-checked allocation-free parse (no unsafe), deterministic CBOR (canonical heads enforced on decode), COSE-inspired signed envelope, ed25519 device signing, shipped C header (rumycelium_env.h) - workspace scaffolding for ingest/calibration/worldgraph/policy/ federation/bench crates Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WNAP3jsEEwgoQLPpMe8kY
…lator + README - rumycelium-ingest: gateway pipeline (envelope decode, registry/revocation, ed25519 verify, DTLS-style anti-replay window that forged packets cannot advance), per-category reject stats (19 tests) - rumycelium-calibration: anchor-rooted lineage chains, affine application with stated uncertainty, EWMA drift detection with sticky quarantine — never silent correction (25 tests) - rumycelium-worldgraph: typed env WorldGraph (sensor/ecosystem/region/ anchor nodes, evidence + contradiction edges, haversine queries, JSON persistence) + RuView FieldEvent RF-context bridge with hard Advisory severity cap and 0.3 evidence-weight cap (12 tests) - rumycelium-bench: deterministic 64-node biome simulator (diurnal signal models, drift/anomaly/outage scenario, tamper/replay/forged-key attack stream) + ADR-264 §14 report scaffolding - dev-profile opt-level=3 for dalek/sha2 so debug tests stay fast - README: RuMycelium section Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WNAP3jsEEwgoQLPpMe8kY
- rumycelium-policy: typestate control path (proposal -> policy -> safety sim -> authority -> signed command -> gateway validation -> receipt); skipping a stage is a compile error; 7-stage audit trail; deterministic ed25519 command signing; execute-once replay protection (14 tests + compile_fail doctest) - rumycelium-federation: OutageBuffer with dedup surviving serialization, Biome sovereignty (global live+replay dedup, unverified-sample rejection, signed DeviceRevoked events, disclosure delay + coordinate coarsening), signed RegionalSummary + FederationBus, OGC SensorThings 1.1 projection with pure-integer RFC3339 (21 tests) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WNAP3jsEEwgoQLPpMe8kY
…arwin-flywheel metaharness - Rename RuMycelium -> RuCelium everywhere (crates rucelium-*, ADR-264, C header rucelium_env.h, spec version rucelium.fabric.v0.1) - rucelium-bench: deterministic 64-node biome simulator (diurnal signal models per modality, drift injection, flood anomaly, 7-day uplink outage, tamper/replay/forged-key/post-revocation attack streams) wired through the REAL production pipeline: ABI -> ingest -> calibration -> WorldGraph + RF context -> biome federation -> SensorThings -> governed control path. ADR-264 §14 scorecard: all 8 criteria pass (SYNTHETIC), 92,460 emissions, 780/780 attacks rejected, 0 restore duplicates, 98.77% usable calibrated observations, p95 alert 0.24 ms - Acceptance tests: full §14 run, same-seed determinism fingerprint, seed-robustness - Event-correlated deviations excluded from drift accounting (a flood must not quarantine healthy sensors; drift is slow and single-sensor) - harness/: rucelium-harness npm metaharness — Darwinian flywheel (vary -> evaluate -> select -> retain) with fitness from workspace tests + clippy + §14 benchmark, generation ledger, strict gate command Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WNAP3jsEEwgoQLPpMe8kY
- docs/ADR-265-rucelium-runtime.md: gateway daemon, compact envelope v2 (LoRaWAN DR0 fit), segmented durable store, federation-over-network, no_std ABI surface decisions - workspace scaffolding for rucelium-store / rucelium-transport / rucelium-gateway (implementations landing in follow-up commits) - README: runtime section with gateway quickstart + two-biome recipe Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WNAP3jsEEwgoQLPpMe8kY
…-265 §2/§3/§5) - rucelium-store: append-only segmented JSONL store — dedup index rebuilt on open, torn-tail crash recovery (truncate-and-continue), deterministic replay, whole-segment retention enforcement, stats (13 tests) - rucelium-transport: compact envelope v2 (114 B packed, pubkey by reference — v1's ~150 B cannot fit LoRaWAN DR0) + 6-byte-header MTU fragmentation with loss/dup/reorder-tolerant reassembler; a compact envelope is exactly 3 DR0 datagrams (25 tests incl. full tamper sweep) - rucelium-abi: std default feature; --no-default-features [--features alloc] compiles the wire format (+ CBOR with alloc) for no_std spore targets; local range constants pinned to the core registry by test Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WNAP3jsEEwgoQLPpMe8kY
…l labeling Review response, part 1: - rucelium-ingest: VerifiedEnvSample — non-serializable, no public constructor, produced only by the full cryptographic verification paths (ingest / reverify_stored); modify() revalidates before committing; reverify_stored() re-checks stored envelopes without touching the replay window (restore path); prime_from_dedup() rebuilds per-device anti-replay windows from a durable dedup index after restart - rucelium-bench report + README: relabelled as fabric REFERENCE-MODEL acceptance — it scores in-memory library components, not the runtime path (store/transport/gateway), which has its own e2e tests Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WNAP3jsEEwgoQLPpMe8kY
Review response, part 2 (blocker 5): - CalibrationAuthority registry with per-modality trust scopes - deterministic CalibrationSigner (canonical-bytes ed25519, signature fields cleared before signing) - strict CalibrationStore::with_authorities: every record in a lineage chain must carry a verifying signature from a signer trusted for its modality — roots and children alike; verify_lineage re-checks each link - the reviewer's attack is a named test: a self-signed record claiming method "anchor_reference" with an unregistered key is rejected - permissive new() retained for tests/simulation, loudly documented - 41 tests (16 new), clippy clean Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WNAP3jsEEwgoQLPpMe8kY
…DR-266 wedges Review response, parts 3-4 (blockers 3 and 7) + deployment strategy: rucelium-store (blocker 3): - persistent dedup index (dedup.idx) is now authoritative on open, so keys survive retention deletion AND restart — the headline flaw - per-record CRC-32: a newline-terminated record whose CRC fails is a hard Corrupt error, never truncated; only a genuinely torn final line (no newline / incomplete CRC prefix) is repaired - opt-in fsync (sync_data on segment + index per append); crate docs now state precisely what each mode guarantees - legacy bare-JSON lines still readable, index backfilled in place - dedup_keys() exposes the durable replay memory (28 tests) rucelium-policy (blocker 7): - safety budget is CHECKED at simulation, CHARGED only on execution, so unauthorized proposals can no longer exhaust another actuator's budget - two-phase execution: Executing -> Executed/Failed; a command in ANY phase is refused (a crashed Executing entry fails closed) - export_phases/restore_phases journal hooks for daemon restart - receipts are now ed25519 attestations signed by the gateway identity, with verify_receipt() (21 tests incl. compile_fail typestate doctest) docs/ADR-266: deployment wedges (flood/watershed first), the biological frontier as a research track with capped-evidence discipline, and the physical 8-16 node acceptance test that supersedes simulation claims Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WNAP3jsEEwgoQLPpMe8kY
…, federation sync The ADR-265 §4 gateway daemon (verified green when written: 24 tests + live smoke run showing DR0 fragment reassembly, calibrated observations, and biome-signed flood alerts): - UDP ingestion dispatching v1 CBOR / v2 compact / DR0 fragment paths - calibration + drift quarantine, WorldGraph registration, disk store - local alert rules emitting biome-signed EnvironmentalEvents - SensorThings-style HTTP API + admin revocation endpoint - peer federation poller: verifies summaries and revocations against the peer's published biome key before applying them - --simulate N synthetic spore swarm (rotates all three encodings) - tests/e2e.rs: two gateways, revocation on A propagates to B, B then rejects the revoked node's traffic WIP NOTE: this commit does not build against the concurrently hardened store/policy/federation APIs (fsync param, two-phase execution, sealed VerifiedEnvSample). Rewiring to those APIs — plus restart-safe replay priming and the kill/restart/resend acceptance test — lands next. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WNAP3jsEEwgoQLPpMe8kY
Review response, part 5 (blockers 4 and 6, plus SensorThings honesty): - Biome::accept now takes only VerifiedEnvSample; AcceptOutcome::Unverified is gone because the state is unrepresentable — a compile_fail doctest proves a bare EnvSample (however its verified flag is set) is rejected by the type system, not by a runtime boolean check - OutageBuffer stores the ORIGINAL SIGNED ENVELOPE, not a bare sample: restore goes envelope -> IngestPipeline::reverify_stored -> accept, so buffered data is re-verified cryptographically after a restart; a tampered buffered envelope fails reverify and never enters the biome - FederationBus binds biome_id -> (pubkey, key_epoch): a registered key claiming another biome's id is IdentityMismatch; higher epoch rotates, lower/equal with a different key is StaleKeyEpoch; duplicate summary windows and duplicate event ids are rejected (bus replay protection) - SensorThings relabelled a *-inspired projection* (not conformant until an external OGC suite passes) and given mandatory fields: Datastream.description + observationType, Sensor.description; encodingType text/plain with the deviation documented - 29 tests (28 unit + compile_fail doctest) Also scaffolds the examples/ workspace member for worked applications. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WNAP3jsEEwgoQLPpMe8kY
examples/ workspace member (rucelium-examples): deterministic Rng, Node provisioning that signs real 48-byte wire records, a Gateway harness over the real IngestPipeline, and narrative output helpers. Sensor values are simulated; the verification machinery is the production code. 4 toolkit tests: sealed sample round-trip, replay rejection, tamper rejection, PRNG determinism. Ten application scenarios land next. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WNAP3jsEEwgoQLPpMe8kY
The reference-model runner now goes through the post-review surfaces: - sealed VerifiedEnvSample from ingest; calibration applied via .modify() - the outage buffer holds ORIGINAL SIGNED ENVELOPES, and every restored envelope is re-verified cryptographically (reverify_stored) before the biome accepts it — strictly more faithful than the previous path, and the duplicate-free assertions still hold - FederationBus::register_biome with an explicit key epoch - GatewayValidator with a gateway identity seed; safety budget charged only after successful execution (record_execution) All 3 acceptance tests green: full §14 run, same-seed determinism fingerprint, seed-robustness. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WNAP3jsEEwgoQLPpMe8kY
Snapshot of the in-flight migration to the hardened APIs so the work is not lost. New modules landing: journal.rs (durable command-phase journal for restart-safe execute-once) and control.rs (governed control path endpoint). Replay-window priming from the store's durable dedup index and the kill/restart/resend acceptance test are part of this migration. The build is red at this commit by construction; the completed, verified migration lands in the next commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WNAP3jsEEwgoQLPpMe8kY
…Q-ready SOTA research outcome. The gap: environmental evidence is retained for years (compliance disputes, climate baselines), but ed25519 signatures lose trustworthiness on that horizon. The naive fix does not fit: scheme signature envelope LoRaWAN DR0 datagrams ed25519 64 B 114 B 3 ML-DSA-44 2,420 B ~2,470 B ~49 ML-DSA's speed is fine; SIZE is the binding constraint at the radio. Decision: split what the signature is doing. Authenticity NOW stays ed25519 per observation at the node (unchanged radio budget). Verifiability LATER moves to a gateway-side Merkle notary that signs only batch ROOTS — amortizing a Dilithium-class signature to well under a byte per observation (4,096-leaf batch: 2,420 B / 4,096 = 0.6 B each), with on-demand inclusion proofs instead of per-record overhead. What ships is algorithm AGILITY, honestly labelled: RuCelium is post-quantum READY, not post-quantum. No ML-DSA implementation is included — a hand-rolled lattice implementation would be worse than none. Roots are self-describing (NotaryAlgorithm recorded inside the signed structure), migration is hybrid dual-signing, and history gains the new guarantee by re-notarization (old roots become leaves of a new PQ-signed tree) without re-signing a single stored observation. Scaffolds crates/rucelium-notary; implementation lands next. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WNAP3jsEEwgoQLPpMe8kY
…st PASSES
Completes the review response. The gateway is migrated to every hardened
API and the restart-attack surface is now closed:
- replay windows are PRIMED FROM THE DURABLE DEDUP INDEX at startup
(ingest.prime_from_dedup(store.dedup_keys())), so a restarted gateway
refuses previously-accepted packets
- command execute-once survives restart via a durable phase journal
(journal.rs: export_phases/restore_phases, fail-closed on Executing)
- strict signed calibration authorities in the daemon
- POST /api/admin/command drives the full governed control path
- --fsync (default on)
tests/restart.rs — the reviewer's acceptance test, all 7 conditions:
1. packet seq 100 rejected after restart PASS
2. command cmd-42 rejected after restart PASS
3. retention-deleted observation still replay-rejected
after restart (durable dedup index) PASS
4. serialized EnvSample with verified=true cannot
enter the biome (type-level) PASS
5. registered federation key cannot claim another
biome identity PASS
6. duplicated signed regional summary rejected PASS
7. corrupted COMPLETE record = integrity error,
not silent truncation PASS
34 unit + 1 e2e + 6 restart tests green.
Also lands the first four application examples (flood-watershed,
irrigation-agriculture, sentinel-forest, ecosystem-immune) — the
remaining six are still being written.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WNAP3jsEEwgoQLPpMe8kY
…vent Surveyed the published ruvnet packages against what RuCelium built bottom-up, and found real duplication to stop: - agentdb ships a single-file .rvf cognitive container with HNSW search, causal graph, and provenance — literally the 'RVF buffering' (§13) and 'RuVector similarity search' (§7) the original ADR named, and exactly the shape ecosystem memory (ADR-266 B8) needs. Binding: a thin rucelium-memory adapter; the append-only store stays the source of truth, the vector container is derived and rebuildable. - agenticow's copy-on-write vector branching (~0.5 ms, 162 bytes, independent of base size) is the right substrate for the ADR-264 §9 SAFETY SIMULATION stage: branch biome state, simulate the actuation, discard the branch — and hand an auditor the branch a decision was made on. Deepens one existing stage; adds none. - the ten Mycelium agents belong on ruflo / agentic-flow. RuCelium ships no agent runtime; it ships what makes agents safe (typed proposals, deterministic policy, authority, signed commands, receipts) and will expose that as an MCP surface. - harness/ and agentic-flow converged independently on 'freeze the model, evolve the harness'; the harness README now credits that explicitly instead of implying novelty. Normative constraint: every binding is ADDITIVE — the §14 acceptance path must keep passing with none of these installed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WNAP3jsEEwgoQLPpMe8kY
Applications complete and passing (39 tests across them):
- flood-watershed lead time vs conventional gauge, blocked culvert,
storm-displaced sensor excluded, RF contradiction
- irrigation-agriculture full governed actuation, verified signed receipt,
unauthorized + over-magnitude proposals stopped
- industrial-compliance regulator-verifiable evidence bundle, strict signed
calibration lineage, independent verifier
- sentinel-forest per-organism bioelectric baselines, confounder that
must NOT escalate, capped evidence
- ecosystem-immune biofilm corroborated by chemical sensors before
escalation, source localization, governed response
- airborne-dna acoustic/genetic confirmation and contradiction,
human-DNA disclosure gate
(four remaining: wildfire-risk, biodiversity-habitat, pollinator-hive,
ecosystem-memory)
Also snapshots in-flight work: rucelium-notary (ADR-267) is mid-write and
its lib.rs references a bundle module not yet on disk — that crate does
not compile at this commit; every other crate is green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WNAP3jsEEwgoQLPpMe8kY
- wildfire-risk: RF-only detection capped at Advisory while physical PM + optical smoke evidence reaches Critical; heat-degraded sensor excluded AND reported rather than silently dropped - pollinator-hive: single-hive collapse stays Advisory (one hive failing is not a regional signal); only time-correlated multi-hive collapse escalates; a merely cold hive produces no event Remaining: biodiversity-habitat, ecosystem-memory. Also snapshots rucelium-notary (ADR-267) mid-iteration — 31 of 33 tests pass; its agent is still fixing the inclusion-proof index/count guard and one bundle assertion. Every other crate is green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WNAP3jsEEwgoQLPpMe8kY
…ly one hop Answers two questions that must not be conflated. Sensor boundary keeps datagrams. QUIC is a category error there: RFC 9000 requires a >=1200-byte padded Initial, ~24x our 114-byte envelope and far past LoRaWAN DR0's 51-byte MTU; a node waking every 30 min cannot amortize a handshake it will have lost by the next wakeup; EU868's 1% duty cycle often makes the round trips unaffordable. Decisively, we do not need what it provides — the envelope is OBJECT-secured (ed25519 over the exact 48 payload bytes), which is what lets an untrusted store-and-forward relay, i.e. a LoRaWAN network server, sit in the path harmlessly. Federation moves poll -> push, and this is a SECURITY fix before a performance one: 30s polling caps revocation propagation at 30s, so a compromised device stays valid at peers for up to a full interval. Push first, transport second, via a FederationTransport trait (announce / subscribe / sync_since) — with the polling backstop MANDATORY so a peer that missed a push still converges. QUIC becomes an optional transport (feature-gated, default off) where it genuinely earns it: connection migration across LTE/satellite/wifi failover, 0-RTT after a partition, per-artifact-class streams so a stalled summary cannot block a revocation, and traffic-analysis resistance — which matters for sensitive-species deployments, where alert TIMING leaks location even when payloads are signed and coordinates coarsened. Two normative constraints: QUIC is defence in depth and NEVER the trust boundary (everything received is verified identically regardless of transport), and TLS identity is the biome's existing ed25519 key rather than a new PKI. The §14 acceptance path and restart-attack tests must pass with the feature disabled. Note: no @ruvector/quic exists — RuVector is a vector database/format family (memory substrate, ADR-268 §2.1), not a transport. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WNAP3jsEEwgoQLPpMe8kY
A REAL production bug, found by the notary work and confirmed by measurement: serde_json's default float parser is fast but not exact. 18,496 of 200,000 realistic sensor values (9.2%) come back one ULP off after a JSON round-trip. Every signature in this workspace is computed over canonical JSON, and a peer verifies by RE-SERIALIZING what it parsed. So a genuine, correctly signed RegionalSummary or EnvironmentalEvent could fail verification at the peer — silently, intermittently, and only in the field. Our tests missed it because they signed and verified in-process, never across a wire round-trip. Fix: pin serde_json's float_roundtrip feature at the workspace level, with a comment stating it is load-bearing. Guard: rucelium-federation now has signed_summary_survives_a_json_wire_ round_trip, which does the real peer path (sign -> to_string -> from_str -> verify) and asserts bit-identical floats. Verified that the guard FAILS without the feature and passes with it. Also lands: - rucelium-notary complete (34 tests): domain-separated Merkle tree (last-node promotion, rejecting the CVE-2012-2459 duplication ambiguity), stateless inclusion proofs, algorithm-agile signed roots, third-party evidence-bundle verification, re-notarization chaining. Measured amortization: 2420 B / 4096 leaves = 0.59 B per observation. It also found that leaf_count must be bound to the SIGNED root, since verify_inclusion alone cannot reject a same-shape count. - all ten worked applications green (60 tests in examples/) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WNAP3jsEEwgoQLPpMe8kY
|
Very interesting. Would be super helpful if you can share the claude code session...the link above appears to be private. |
Closes a schema gap the compliance example exposed while being built. EnvironmentalEvent.evidence is a list of EvidenceRef — (node_id, sequence) — which pins WHICH observations were cited but never their CONTENT. Two different readings with the same identity are indistinguishable, so editing an observation's value inside an exported evidence bundle broke nothing: the event signature still verified. The industrial-compliance example had to smuggle a sha256 through the signed message STRING and parse it back out to get tamper-evidence — a workaround standing in for a missing field. Adds EnvironmentalEvent.evidence_digest plus rucelium_core:: evidence_digest(), which hashes each cited observation's canonical JSON LENGTH-PREFIXED and in citation order — length prefixing so two different citation lists cannot concatenate to the same byte stream, order-sensitivity because a reordered evidence list is a different claim. Because the field sits inside the signed structure, altering any cited observation now invalidates the signature. Producers updated to bind real content (gateway alerts, bench alerts, compliance bundles); events that make no content claim (DeviceRevoked) carry None. The compliance verifier prefers the structured field and keeps the message-string path only as a documented legacy fallback. 412 tests green across the workspace (gateway excluded — its QUIC/push federation work is still in flight), clippy clean, fmt clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WNAP3jsEEwgoQLPpMe8kY
Snapshot of in-flight work: the FederationTransport abstraction, push-on-revocation, and the optional QUIC transport. The agent is actively editing, so this commit captures a moving target and the crate does not build at this SHA (a push-announce call site is mid-rename). Every other crate is green: 412 tests, zero clippy warnings, fmt clean. The verified gateway lands in the next commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WNAP3jsEEwgoQLPpMe8kY
Federation is no longer a poller. A FederationTransport abstraction (announce / subscribe / sync_since) carries signed artifacts, with push-on-revocation so a compromised device's revocation propagates at link speed instead of waiting up to a full polling interval — that interval was a security window, not just latency. The polling backstop stays MANDATORY (ADR-269 §3): a peer that missed a push must still converge, so sync_since runs on reconnect and on a slow timer regardless of transport. Everything received is verified identically no matter how it arrived — the transport is never the trust boundary. 466 tests green across the workspace, zero clippy warnings, fmt clean. The optional QUIC transport (§4) is still being written. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WNAP3jsEEwgoQLPpMe8kY
…pinning Completes ADR-269. The optional QUIC transport (feature-gated, default off) binds TLS identity to the biome's EXISTING ed25519 key — genuine RFC 7250 raw public keys, no X.509 anywhere, no new PKI, no certificate authority, no name-based trust. How the pin actually holds: - server wraps the same biome seed Biome signs with as PKCS#8 (RFC 8410) and serves a bare SubjectPublicKeyInfo; a test asserts the advertised key equals Biome::public_key_hex() — one key, not two - client builds the expected SPKI with rustls' own helper (byte-identical to what a peer serves, asserted), accepts only an exact match, rejects intermediates, re-checks the pin inside verify_tls13_signature, refuses TLS 1.2, and advertises only Ed25519 - requires_raw_public_keys() = true means a server that doesn't negotiate RawPublicKey gets a fatal handshake error — an X.509 answer cannot be accepted, so there is no downgrade path - the custom verifier is STRICTER than webpki (one key, exact bytes, no CA, no name matching) and never returns Ok for an unpinned key The decisive test: a_peer_presenting_the_wrong_key_is_refused_and_ delivers_nothing — the mismatch names both keys and delivers zero artifacts, then the same endpoint dialled with the correct key succeeds, proving the refusal was the pin and not a connection failure. Per §4.3 each artifact class gets its own stream, so a large summary transfer cannot stall a revocation. Known limitations, documented rather than hidden: the server does not authenticate clients (safe only because the session is never the trust boundary — an unauthenticated peer can waste bandwidth, never revoke a device); 0-RTT resumption is not wired; and main.rs has no --quic-listen flag yet, so the daemon still federates over HTTP. 470 tests green (58 default / 67 with --features quic), zero clippy warnings in both configurations, fmt clean. The §5 regression guard holds: restart.rs 6/6 and e2e.rs 1/1 pass unchanged with the feature off. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WNAP3jsEEwgoQLPpMe8kY
…-270) SECURITY. Found by asking the 20-year question in the honest ledger — 'could a stranger verify this after the institution that owned it is gone?' Chasing that amber row surfaced a live vulnerability. FederationBus::register_biome accepted ANY strictly-higher epoch and rebound the identity to the presented key, with no proof of continuity: the incoming key was never signed by the outgoing one. So a peer — configured, malicious, or merely compromised — could announce biome/thames-estuary at epoch 999 with its own key, and from then on the gateway accepts the ATTACKER's summaries and revocations as that biome's while rejecting the real biome's as an IdentityMismatch. The identity-binding hardening was doing real work; rotation walked around it. Fix follows TUF root rotation: new keys become trusted only via a statement signed by currently-trusted keys. - register_biome is now GENESIS ONLY (trust-on-first-use, idempotent for an unchanged key); rebinding an established identity returns SuccessionRequired - rotation moves to rotate_biome(&KeySuccession), signed over canonical bytes that include from_epoch — so a captured succession cannot be replayed onto a later state - two authorisation paths, no third: CONTINUITY (outgoing key signs) or RECOVERY (m-of-n distinct pre-declared custodians) The recovery path is the point. Over twenty years an institution being restructured, defunded, merged, or simply losing its key is the expected case, not an edge case. A 2-of-3 custodian quorum can hand the identity to a successor without the original key ever existing again — which is the only way a 2026 baseline is still citable in 2046. Successions can also rotate the custodian set, so governance evolves without breaking the chain. threshold=0 opts out explicitly: the identity dies with its key, by choice rather than by accident. 10 new tests, led by an_unsigned_epoch_bump_cannot_steal_an_identity and custodians_can_recover_an_identity_whose_holder_is_gone. The pre-existing rotation test failed after the fix because it exercised the vulnerable path — rewritten against the succession API. 479 tests green, zero clippy warnings, fmt clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WNAP3jsEEwgoQLPpMe8kY
Third bug of the same family, found by continuing to probe serialization
edges. JSON has no NaN and no Infinity: serde_json writes BOTH as null,
indistinguishably, and parsing null back into an f32/f64 fails outright.
Measured:
SERIALIZED: {"confidence":null,"sig":null}
LOCAL VERIFY MATCHES: true <- signs and verifies in-process
WIRE: {"confidence":null,"sig":"deadbeef"}
PARSE FAILED: invalid type: null, expected f32
So a signature over a non-finite float is worse than no signature: it
looks valid locally and is unparseable at the peer it was minted for —
signable but undeliverable, and silent about it.
Fix, stated as the invariant rather than a special case: SIGN ONLY WHAT
ROUND-TRIPS. New crate::round_trips serializes, parses back, and requires
equality — which catches NaN and Infinity today and any future
serialization hazard for free. Both signing paths (Biome::sign_event,
Biome::sign_summary) now fail closed: they leave the artifact unsigned
rather than mint an unusable signature, so FederationBus rejects it as
Unsigned instead of shipping something that cannot be verified.
Scope, honestly: no current call site produces a non-finite value —
summarize() cannot (an accumulator exists only with >=1 sample, so no
0/0, and sample values are validated finite before acceptance, asserted
by a new test over four windows including empty ones). This closes a
latent hazard on a public API, not an active outage.
4 new tests. Verified load-bearing by disabling the guard and watching
a_non_finite_float_is_never_signed fail, then restoring it.
483 tests green, zero clippy warnings, fmt clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WNAP3jsEEwgoQLPpMe8kY
|
Reviewed as part of clearing the outstanding queue (#3, #4, #5, #7 are now merged). Not merging this one, because its own description carries a standing hold:
That is a deliberate decision with a stated reason, and it is not mine to overturn — a simulator passing its acceptance test is not the same claim as a fabric that has run on real hardware, and this PR is explicit that it only has the former. Two practical notes for whenever it is picked up:
Say the word if the hold should be lifted and I will rebase and merge it. |
What this is
RuCelium extends the RuField stack from room-scale sensing to planetary environmental sensing — deliberately not as a flat global peer mesh (which fails on battery, bandwidth, routing, calibration, sovereignty, and compromised nodes) but as a federated fabric with four sovereignty layers:
C stays confined to the sensor boundary; everything above is safe Rust (
unsafe_code = "forbid"workspace-wide — the C wire format is parsed, never transmuted). RF joins as a contextual modality with a hardAdvisoryseverity cap.Decision records
no_stdABICrates (14) + 10 worked applications
Model & boundary —
rucelium-core(EnvSample with twelve mandatory attributes, calibration lineage, events),rucelium-abi(packed 48-byterv_env_sample_v1, bounds-checked alloc-free parse, deterministic CBOR, ed25519;no_stdcapable),rucelium-transport(114-byte envelope v2 + DR0 fragmentation).Gateway —
rucelium-ingest(signature → revocation → anti-replay),rucelium-calibration(signed authorities, drift quarantine),rucelium-worldgraph(evidence + contradiction edges, RF bridge),rucelium-store(durable segmented log),rucelium-policy(governed control path),rucelium-notary(Merkle notarization),rucelium-federation(sovereignty, SensorThings projection),rucelium-gateway(the daemon).Proof & tooling —
rucelium-bench(64-node reference-model acceptance),examples/(10 runnable applications),harness/(npm Darwin flywheel).Response to the security review
All eight merge blockers closed:
ExecutingverifiedbooleanVerifiedEnvSample— non-serializable, no public constructor; forging it fails to compileCalibrationAuthorityregistry with modality scopes; every record in a chain must verifybiome_id → key + epoch; cross-biome claims, duplicate summaries and duplicate events rejectedThe reviewer's acceptance test — all 7 conditions pass
crates/rucelium-gateway/tests/restart.rsTwo real bugs found and fixed while building
Exact float parsing is load-bearing.
serde_json's default float parser is fast but not exact — measured, 18,496 of 200,000 (9.2%) realistic sensor values return one ULP off after a round-trip. Every signature here is computed over canonical JSON and peers verify by re-serializing what they parsed, so genuine signed summaries could fail verification at the peer — silently, intermittently, in the field only. Existing tests missed it by verifying in-process. Fixed workspace-wide; guarded by a test that does the real peer path and was verified to fail without the fix.Evidence refs pin identity, not content.
EvidenceRefis(node_id, sequence), so editing an observation's value inside an exported evidence bundle left the event signature valid. AddedEnvironmentalEvent.evidence_digest(length-prefixed, order-sensitive hash of cited observations' canonical JSON) so the signature now covers content.Reference-model acceptance (SYNTHETIC)
cargo run -p rucelium-bench— all 8 ADR-264 §14 criteria pass: 92,460 emissions over 30 simulated days, 780/780 tamper/replay/forged/post-revocation attacks rejected, 21,504 samples buffered through a 7-day outage and restored with 0 duplicates, p95 alert latency 0.131 ms, 100% SensorThings + WorldGraph mapping, revocation without interruption, 98.77% usable calibrated observations.Post-outage restore is now stronger than before the review: buffered envelopes are re-verified cryptographically on restore, so tampering at rest is caught.
Post-quantum readiness (ADR-267)
Environmental evidence outlives its cryptography. Signing each reading post-quantum is infeasible at the radio:
So authenticity stays ed25519 per observation, and verifiability moves to gateway-side Merkle notarization: one root signature over a batch costs 0.59 bytes per observation at 4,096 leaves, with on-demand inclusion proofs. Honestly labelled: post-quantum ready, not post-quantum — no ML-DSA implementation ships, because a hand-rolled lattice implementation would be worse than none.
Verification
cargo clippy --all-targetszero warnings,cargo fmtclean,unsafe_code = "forbid".Honest status
~70% as an architectural specification, ~55% as a software platform, ~15% as a deployable physical system. Not shipped: real C firmware, real radios, a conformant SensorThings server, multi-biome field federation, field calibration evidence. Per ADR-266 §6 this becomes a product when one physical biome of 8–16 nodes runs 30 days, survives an outage and a restart, rejects replays after that restart, catches a drifting sensor, preserves signed calibration lineage, and produces one independently verifiable event.
🤖 Generated with Claude Code
https://claude.ai/code/session_016WNAP3jsEEwgoQLPpMe8kY