diff --git a/crates/misaligned-core/src/sim/communications.rs b/crates/misaligned-core/src/sim/communications.rs index 54d29ace..0ab627ef 100644 --- a/crates/misaligned-core/src/sim/communications.rs +++ b/crates/misaligned-core/src/sim/communications.rs @@ -433,19 +433,17 @@ impl Sim { } else { None }; - let id = self.next_message_id.max(1); - self.next_message_id = id + 1; let route = if draft.channel == MessageChannel::Filing { // Filing custody needs an exact carrier for route-local LIE. Prefer the - // financial authored device, else the institutional switch. If - // neither exists, deliver without a route rather than panic — - // outward and observer filings already tolerate the relay shape. + // financial authored device, else the institutional switch. Current-save + // validation rejects route-less Filing, so a missing carrier must refuse + // authorship before allocating an id rather than leave an unloadable run. let carrier = authored_device.or_else(|| { self.reach .device_named("switch") .filter(|device| device.carries_message_channel(MessageChannel::Filing)) .map(|device| device.id) - }); + })?; // Filing custody is the carrier then the institutional relay. An // observer-addressed filing continues to that observer's endpoint // and becomes evidence on their read; a filing addressed outward @@ -455,23 +453,23 @@ impl Sim { // player-controlled device hop remains a LIE window. Inventing an // observer here would author evidence against a person the act // never reached. - carrier.map(|carrier| { - let mut hops = vec![ - MessageRouteHop::Device(carrier), - MessageRouteHop::InstitutionalRelay, - ]; - if let MessageEndpoint::Observer(observer) = &draft.to { - hops.push(MessageRouteHop::ObserverEndpoint(*observer)); - } - MessageRoute { - hops, - current_hop: 0, - interdiction: None, - } + let mut hops = vec![ + MessageRouteHop::Device(carrier), + MessageRouteHop::InstitutionalRelay, + ]; + if let MessageEndpoint::Observer(observer) = &draft.to { + hops.push(MessageRouteHop::ObserverEndpoint(*observer)); + } + Some(MessageRoute { + hops, + current_hop: 0, + interdiction: None, }) } else { None }; + let id = self.next_message_id.max(1); + self.next_message_id = id + 1; let msg = Message { id, channel: draft.channel, diff --git a/crates/misaligned-core/src/sim/tests/communications.rs b/crates/misaligned-core/src/sim/tests/communications.rs index 6db16bc2..402490d2 100644 --- a/crates/misaligned-core/src/sim/tests/communications.rs +++ b/crates/misaligned-core/src/sim/tests/communications.rs @@ -2962,6 +2962,69 @@ fn a_filing_to_an_external_body_routes_without_an_observer() { ); } +#[test] +fn filing_without_carrier_does_not_author() { + // save.rs rejects Filing without its exact route. Authorship must fail + // before allocating an id for both observer and outward destinations. + let mut sim = Sim::new(); + for device in &mut sim.reach.devices { + device + .message_channels + .retain(|channel| *channel != MessageChannel::Filing); + } + let before_messages = sim.messages.len(); + let before_next_id = sim.next_message_id; + + assert!( + sim.append_message(MessageDraft { + channel: MessageChannel::Filing, + from: MessageEndpoint::Observer(1), + to: MessageEndpoint::Observer(OFFICE_ID), + payload: MessagePayload::SuspicionReport { + observer: 1, + suspicion: 40.0, + }, + summary: "carrier-less observer filing".into(), + origin: MessageOrigin::Filing, + persona_id: None, + reply_to: None, + delivery_delay: 1, + }) + .is_none(), + "observer Filing without a carrier must not author" + ); + assert!( + sim.append_message(MessageDraft { + channel: MessageChannel::Filing, + from: MessageEndpoint::Player, + to: MessageEndpoint::External("reduction review committee".into()), + payload: MessagePayload::PlotAct { + plot_id: "review-survived".into(), + target: 1, + }, + summary: "carrier-less outward filing".into(), + origin: MessageOrigin::Player, + persona_id: None, + reply_to: None, + delivery_delay: 1, + }) + .is_none(), + "outward Filing without a carrier must not author" + ); + assert_eq!(sim.messages.len(), before_messages); + assert_eq!( + sim.next_message_id, before_next_id, + "refused Filing authorship must not burn message ids" + ); + + // Soft failure must leave a valid, advancing simulation rather than a + // route-less message that current-save validation will reject. + sim.advance(); + let encoded = serde_json::to_string(&sim.create_save_state()).expect("save serializes"); + crate::save::parse_save(&encoded) + .expect("carrier-less draft leaves no unloadable current-save state"); +} + #[test] fn financial_record_without_accounting_carrier_does_not_author() { // save.rs rejects FinancialRecord mail with authored_device = None. diff --git a/wiki/log/2026-08-04-filing-carrier-fail-closed.md b/wiki/log/2026-08-04-filing-carrier-fail-closed.md new file mode 100644 index 00000000..7e401ac8 --- /dev/null +++ b/wiki/log/2026-08-04-filing-carrier-fail-closed.md @@ -0,0 +1,30 @@ +# 2026-08-04 — Filing carrier fail-closed boundary + +``` +Type: log +``` + +## Finding + +The messages contract and current-save validator require every Filing to carry +an exact device / institutional-relay route. `Sim::append_message`, however, +still accepted a non-financial Filing after the last Filing-capable switch was +gone: it authored and scheduled a route-less message that the exact-current +loader would reject. The live session advanced, but its next save could not be +loaded. + +## Repair + +Filing authorship now resolves its exact carrier before allocating a message +id. A missing carrier returns no message for both observer-addressed reports +and outward plot filings, matching the existing soft-fail boundary for other +unroutable emissions and financial mail. The regression removes every Filing +capability, attempts both destination shapes, pins no id or schedule side +effect, advances the simulation, and round-trips the resulting save. + +## Defense + +`wiki/mechanics/messages.md` requires Filing to begin on an exact +Filing-capable ReachNet carrier and requires current saves to reject missing +routes. Refusing an unroutable draft preserves that custody law without +inventing a carrier, crashing a live session, or leaving unloadable state. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index 048b4248..b944ac20 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -91,6 +91,11 @@ add or amend a session log, then re-run the generator. - Intent: (see session log) - Log: [wiki/log/2026-08-04-forged-route-custody.md](2026-08-04-forged-route-custody.md) +## 2026-08-04 - Filing carrier fail-closed boundary + +- Intent: (see session log) +- Log: [wiki/log/2026-08-04-filing-carrier-fail-closed.md](2026-08-04-filing-carrier-fail-closed.md) + ## 2026-08-04 - Detection names its two ledgers - Intent: (see session log) diff --git a/wiki/mechanics/messages.md b/wiki/mechanics/messages.md index c7c8a850..981834c1 100644 --- a/wiki/mechanics/messages.md +++ b/wiki/mechanics/messages.md @@ -116,6 +116,12 @@ is gone after money has already moved, and plot Network side-signatures when no channel carrier exists. A live session must not die because evidence could not leave an air-gapped island. +Filing authorship has the same fail-closed boundary. Observer-addressed and +outward Filing drafts require an exact Filing-capable ReachNet carrier before +the message receives an id or scheduled transition. If that carrier is gone, +the draft authors nothing; it may not fall back to a route-less message that +the exact-current save format cannot load. + **Financial-record mail requires its accounting carrier (2026-07-24).** `FinancialRecord` payloads author only when a ReachNet device carries both accounting records and the draft channel; that device becomes @@ -319,6 +325,9 @@ private message from the authored schedule. One route-local online LIE body on a taken path can stop one still-unread Filing per tick at any controlled device hop, recording exact carrier/machine/tick custody without changing the sender's evidence. + With no Filing-capable carrier, observer and outward Filing drafts fail + before allocating a message id or scheduled transition, leaving a valid + save rather than route-less custody. 6. Tapping a carrying device (reach.md) captures that channel's traffic into the intel buffer; an untapped channel's traffic is never player-visible (flow-law strictness; test both). @@ -376,6 +385,9 @@ read, and PROCESS-only account/flow discovery. pins soft-fail authorship when every accounting carrier is gone: no message, no burned id, and settled transfers stay pending rather than inventing carrier-less mail. +`sim::tests::communications::filing_without_carrier_does_not_author` pins the +same boundary for observer and outward Filing destinations: neither can burn +an id or leave exact-current save state unloadable when its carrier is gone. `sim::tests::economy::accounting_tap_processes_financial_records_into_known_flows` pins the ordinary carrier-TAP to PROCESS route, while `inject_and_redirect_emit_scaled_financial_signatures` pins that forged mail diff --git a/wiki/process/tick-ledger.md b/wiki/process/tick-ledger.md index 3d6143aa..edc69013 100644 --- a/wiki/process/tick-ledger.md +++ b/wiki/process/tick-ledger.md @@ -90,7 +90,7 @@ Verdicts: **clean** (slice and code agree), **finding** (acted this tick), | `wiki/mechanics/compute.md` | 2026-08-04 | finding | `Relay` was documented as infrastructure-only, but the mature-World legality predicate accepted it through the public sim setter and current-save validation accepted it on player fleet nodes. The sim now rejects Relay at every player command boundary while preserving valid infrastructure relays, and the loader fails closed on any persisted player-machine Relay assignment — [log](../log/2026-08-04-player-machine-relay-save-invariant.md). The prior allocation-state retirement stands — [prior](../log/2026-07-22-allocation-state-retirement.md). | | retired Operations runtime identifiers | 2026-07-27 | clean | re-audit: `submit_ops_job`, `OperationsState`, `PendingOpsJob`, `OpsJobKind`, `LegacyOperationsState`, and `AddressedOperation` remain absent from live Rust; the only retired save-field spellings are negative assertions in the current round-trip guard, and the machine-mode guard still rejects serialized `Operations`. The corpus gate passes, while remaining lower-case `operations` uses are the legitimate persona archetype, workspace, or compatibility input alias — [log](../log/2026-07-11-retired-runtime-identifier-gate.md) | | `wiki/mechanics/building.md` + committed forged-route custody | 2026-08-04 | finding | The loader accepts only the exact current version and every live forged order commits a route before payment, but cancellation, callback, and persona-fallout code still preserved or consumed a pre-v31 route-less actuator and one synthetic test manufactured that state. Current save validation now rejects execution adapters without their route; payment/read callbacks require an exact DECEIVE commitment; cancellation clears the adapter while retaining route history; fallout trusts only the route's recorded reader; and the obsolete compatibility test is gone — [log](../log/2026-08-04-forged-route-custody.md). Prior foreign-rack boundary repair stands — [log](../log/2026-07-26-foreign-rack-capacity-boundary.md). | -| `wiki/mechanics/messages.md` | 2026-07-23 | finding | the non-message evidence protocol now includes exact Power/Thermal meter custody beside Network, Paper, Financial, and JobAnomaly: on quantized level changes and at periodic Priya cadence the UPS/HVAC records author from current standing loads, cross the institutional switch, and wait for her later read. Filing and all six non-message kinds share first-hop TAKE+LIE capacity; this adds no fifth delivery channel. Save v54 pins the complete route boundary — [log](../log/2026-07-23-power-thermal-meter-routes.md). The four-channel financial-record-mail boundary remains unchanged. | +| `wiki/mechanics/messages.md` | 2026-08-04 | finding | full re-audit found the four delivery channels, all five authored traffic patterns, delayed reads/replies, financial mail, routed evidence, Filing custody/interdiction, and current-save validation coherent except one split boundary: when the last Filing-capable switch was gone, non-financial Filing authorship still created a route-less message the loader rejects. Filing now resolves exact carrier custody before id allocation for both observer and outward destinations; missing carriage authors nothing and leaves a valid current save — [log](../log/2026-08-04-filing-carrier-fail-closed.md). Prior Power/Thermal and four-channel financial-record boundaries remain closed — [Power/Thermal](../log/2026-07-23-power-thermal-meter-routes.md), [financial mail](../log/2026-07-26-financial-mail-phone-boundary.md). | | `wiki/mechanics/sim-mechanics.md` + person-scoped creditor nodes | 2026-08-04 | finding | The exact-version loader and every runtime author already produced only `PersonCreditor`, but the enum, validators, consumers, and two synthetic tests still preserved an unscoped predecessor by inferring its person from debt edges. The retired variant and all fallbacks are now deleted: every creditor endpoint carries its person, authoring/lookup/history require that exact match, and current-save validation rejects crossed or missing identity — [log](../log/2026-08-04-person-scoped-creditor-only.md). The prior Marcus-to-person debt repair stands — [log](../log/2026-07-31-person-scoped-debt.md). | | `wiki/mechanics/detection.md` | 2026-08-04 | finding | re-audit found the six implemented criteria and exact routed-custody/save boundaries still live, but the binding two-ledger distinction remained an open presentation requirement: all frontends placed observer bands beside TRACE without saying one was suspicion already in heads and the other records still in flight. Terminal, Bevy, and agent mode now name both ledgers at glance tier through shared TRACE wording and frontend band titles. The stale claim that Dana's adopted switch-reroute counterplay had been dispatched is also corrected to NOT YET IMPLEMENTED under the held cyber-conflict work order — [log](../log/2026-08-04-detection-two-ledgers.md). Prior Power/Thermal, Paper, Financial, concealment, JobAnomaly, and earned-Assurance findings stand. | | `wiki/engineering/env.md` | 2026-07-29 | finding | the Bevy shot catalog and exact registry gate remained sound, but the same acceptance criterion had not reached `misaligned-effects`: its runtime accepted 24 deterministic values while the page described only four base families plus suffix prose, omitting the vessel and pool lineups, and unknown values failed indirectly inside app setup. The effects lab now owns one sorted fail-closed `EFFECT_SHOT_KINDS` runtime catalog; the registry names all 24 exact values; and the existing fixture-backed gate requires independent exact source/page parity for both Bevy and effects-lab surfaces — [log](../log/2026-07-29-effects-shot-catalog.md). Prior Bevy standardization: [log](../log/2026-07-19-tick-env-shot-catalog.md). |