From f7170fadf907a768401159278b5efa329a671f64 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 16 Sep 2026 22:49:49 +0200 Subject: [PATCH] Give the instance check one owner and drop the dead serde mirror Ruling 14's comparison moves out of `admit_steps` into `Mind::require_instance`, so admission and the CultNet surface's reads ask one function rather than two copies of one rule. A1 calls it where the inline comparison was and behaves as before. The leaf derives `Serialize`, `Deserialize` and `JsonSchema` for `PipelineRefusal` at the pinned revision, so serde's remote definition of it is deleted and `MindRefusal::Document` wraps the leaf's value directly. `PipelineAdmissionBatch` derives the same three and loses the doc sentence that claimed it could not. The crate re-exports `epiphany_pipeline` and the owned redb store type, so the daemon and the client name the leaf's types and the store through one dependency entry at one revision instead of declaring the git dependency twice. Co-Authored-By: Claude Opus 5 --- crates/huginn-mind/src/admission.rs | 10 +++------ crates/huginn-mind/src/lib.rs | 7 +++++- crates/huginn-mind/src/mind.rs | 35 +++++++++++++++++++++++++++++ crates/huginn-mind/src/refusal.rs | 23 +------------------ crates/huginn-mind/src/store.rs | 7 +++++- tools/eureka-cut8-mutations.psd1 | 8 +++++-- 6 files changed, 57 insertions(+), 33 deletions(-) diff --git a/crates/huginn-mind/src/admission.rs b/crates/huginn-mind/src/admission.rs index b942e05..5240cb6 100644 --- a/crates/huginn-mind/src/admission.rs +++ b/crates/huginn-mind/src/admission.rs @@ -47,10 +47,8 @@ use crate::store::MindStore; pub const BATCH_MAX: usize = 64; /// One admission request: the mind it is for, who asks, and the documents. -/// The leaf's `PipelineDocument` derives neither `Serialize` nor -/// `JsonSchema` at the pinned rev, so this type cannot either; the wire -/// shape is Cut 10's. -#[derive(Clone, Debug, PartialEq, Eq)] +/// Rides the wire whole (`wire::HuginnMindRequest::Admit`). +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct PipelineAdmissionBatch { pub instance: Slug, pub provenance: PipelineProvenance, @@ -108,9 +106,7 @@ impl Mind { now: DateTime, ) -> Result { // A1 - if instance != self.instance() { - return Err(MindRefusal::ForeignInstance { declared: instance.0.clone(), mind: self.instance().0.clone() }); - } + self.require_instance(instance)?; // A2 if envelopes.is_empty() || envelopes.len() > BATCH_MAX { return Err(MindRefusal::BatchSize { actual: envelopes.len() as u32 }); diff --git a/crates/huginn-mind/src/lib.rs b/crates/huginn-mind/src/lib.rs index 45a0606..20345ee 100644 --- a/crates/huginn-mind/src/lib.rs +++ b/crates/huginn-mind/src/lib.rs @@ -39,4 +39,9 @@ pub use query::{ }; pub use receipt::{DocumentVersion, Faculty, HuginnCommitReceipt, PipelineProvenance, RECEIPT_SCHEMA_VERSION}; pub use refusal::MindRefusal; -pub use store::MindStore; +pub use store::{MindStore, OwnedRedbMessagePackBackingStore}; + +/// The leaf, whole, through the one crate that pins its rev: the daemon and +/// the client name `Slug`, `PipelineRef` and the document types from here, so +/// one `[dependencies]` entry decides which revision the workspace speaks. +pub use epiphany_pipeline; diff --git a/crates/huginn-mind/src/mind.rs b/crates/huginn-mind/src/mind.rs index 12f7961..bf40e16 100644 --- a/crates/huginn-mind/src/mind.rs +++ b/crates/huginn-mind/src/mind.rs @@ -121,6 +121,20 @@ impl Mind { &self.instance } + /// Ruling 14 across every transport: the declared instance is this mind's, + /// else `ForeignInstance { declared, mind }`. A1 for admission; the daemon + /// asks it for every read that names an instance, and compares nothing + /// itself. + pub fn require_instance(&self, declared: &Slug) -> Result<(), MindRefusal> { + if declared != self.instance() { + return Err(MindRefusal::ForeignInstance { + declared: declared.0.clone(), + mind: self.instance().0.clone(), + }); + } + Ok(()) + } + /// The image: every envelope the store held at the last pull, in /// identity order. Cut 10's snapshot source reads this. pub fn envelopes(&self) -> &[CultCacheEnvelope] { @@ -388,6 +402,27 @@ mod tests { assert_eq!(mind.envelopes().len(), 2); } + /// One check, one owner: the value admission refuses a foreign instance + /// with is the value `require_instance` returns, so the daemon asking it + /// for a read and `admit_steps` asking it for a write cannot disagree. + #[test] + fn require_instance_is_the_one_check_admission_and_the_daemon_share() { + use crate::fixtures::{OTHER_INSTANCE, now, provenance, seeded}; + use crate::receipt::Faculty; + + let mut mind = seeded(); + let foreign = MindRefusal::ForeignInstance { declared: OTHER_INSTANCE.into(), mind: INSTANCE.into() }; + assert_eq!(mind.require_instance(&slug(OTHER_INSTANCE)).err(), Some(foreign.clone())); + assert_eq!(mind.require_instance(&slug(INSTANCE)), Ok(())); + + let batch = crate::admission::PipelineAdmissionBatch { + instance: slug(OTHER_INSTANCE), + provenance: provenance(Faculty::Hands), + documents: vec![instance(OTHER_INSTANCE)], + }; + assert_eq!(mind.admit(batch, now()), crate::admission::PipelineAdmissionOutcome::Refused(foreign)); + } + #[test] fn a_mind_has_one_owner_at_a_time() { let root = tempfile::tempdir().unwrap(); diff --git a/crates/huginn-mind/src/refusal.rs b/crates/huginn-mind/src/refusal.rs index 3f6846a..2087414 100644 --- a/crates/huginn-mind/src/refusal.rs +++ b/crates/huginn-mind/src/refusal.rs @@ -17,11 +17,7 @@ use serde::{Deserialize, Serialize}; /// still in force. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub enum MindRefusal { - Document( - #[serde(with = "DocumentRefusal")] - #[schemars(with = "DocumentRefusal")] - PipelineRefusal, - ), + Document(PipelineRefusal), ForeignInstance { declared: String, mind: String }, MissingIdentity, ForeignEpoch { found: String, expected: String }, @@ -61,23 +57,6 @@ pub enum MindRefusal { Unavailable { detail: String }, } -/// The leaf's `PipelineRefusal` at the pinned rev derives neither `Serialize` -/// nor `JsonSchema`. This is serde's remote definition of it: the same four -/// variants, field for field, compiled against the leaf's type, so a leaf -/// change that renames or adds a field stops this crate building rather than -/// drifting. It is a projection for the wire, not a second vocabulary; the -/// refusal itself is still the leaf's value. When the leaf derives the two -/// traits this goes. -#[derive(Serialize, Deserialize, JsonSchema)] -#[serde(remote = "PipelineRefusal")] -#[allow(dead_code)] -enum DocumentRefusal { - FieldBound { field: String, limit: u32, actual: u32 }, - InvalidFormat { field: String, value: String }, - InvalidIdentity { kind: PipelineKind, key: String, expected: String }, - ForeignStore { r#type: String }, -} - impl std::fmt::Display for MindRefusal { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(formatter, "mind refusal: {self:?}") diff --git a/crates/huginn-mind/src/store.rs b/crates/huginn-mind/src/store.rs index d532fbd..58a3d66 100644 --- a/crates/huginn-mind/src/store.rs +++ b/crates/huginn-mind/src/store.rs @@ -5,7 +5,12 @@ //! atomicity test. Nothing else is abstracted. use anyhow::Result; -use cultcache_rs::{CacheBackingStore, CultCacheEnvelope, OwnedRedbMessagePackBackingStore}; +use cultcache_rs::{CacheBackingStore, CultCacheEnvelope}; + +/// The one store a daemon opens. Re-exported so the crate that serves a mind +/// names the store type without declaring `cultcache-rs` a second time: one +/// dependency entry, one rev, one place to move it. +pub use cultcache_rs::OwnedRedbMessagePackBackingStore; /// A store a mind can be opened over. `Clone` shares ownership of the same /// store (the owned redb store's clones share its lock and handle), which is diff --git a/tools/eureka-cut8-mutations.psd1 b/tools/eureka-cut8-mutations.psd1 index 4c8f053..17f1217 100644 --- a/tools/eureka-cut8-mutations.psd1 +++ b/tools/eureka-cut8-mutations.psd1 @@ -36,14 +36,18 @@ # and H67 by file alone, and H47 and H60 re-anchored on the scope selectors # `assignments_of` and `resolutions_of` the move extracted. Their rules are # unchanged. +# +# Cut 10 moved A1's comparison into `Mind::require_instance`, which admission +# and the daemon's reads now share, so H1 anchors on `mind.rs`. One mutant, +# two suites, one owner: the same edit is `D1` in `eureka-cut10-mutations.psd1`. @{ Mutations = @( @{ Id = 'H1' Rule = 'Ruling 14: admission refuses another instance''s identity (A1).' Test = 'admission::tests::admission_refuses_a_foreign_instance_whatever_the_transport' - File = 'crates/huginn-mind/src/admission.rs' - Old = ' if instance != self.instance() {' + File = 'crates/huginn-mind/src/mind.rs' + Old = ' if declared != self.instance() {' New = ' if false {' } @{