From 4fba6cc6d4bc1cd5ef325ee9474268c120c798fe Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 16 Sep 2026 23:48:10 +0200 Subject: [PATCH] Pin the instance check against names that resemble the mind's own Every fixture pair differed in length, so a comparison of lengths, of first bytes, or one gated on the declared name being the longer passed both suites. The check was pinned by the fixtures' luck, not by the test. Two foreign names now stand beside `thought-cage` in the mind test and the daemon test: `yggdrasix`, which is `yggdrasil`'s own length and differs in its last byte alone, and `yggdrasil-two`, which carries the whole name as its prefix. The near name kills the three comparisons above; the prefixed one kills a `starts_with`. `D1L` is the loosening the brief asked for, comparing lengths, restored to that shape; `D1L2` compares first bytes, `D1L3` runs the gate only when the declared name is longer, `D1L4` asks whether one name begins with the other, and `D1DL` runs the length comparison against the daemon suite so the daemon test's own fixtures are load-bearing. The refusal-naming mutant that had taken `D1L`'s place is kept as `D1N`; it attacks a weaker rule and is no longer standing in for one. Co-Authored-By: Claude Opus 5 --- crates/huginn-daemon/src/daemon.rs | 25 +++++++++++++++++ crates/huginn-mind/src/fixtures.rs | 8 ++++++ crates/huginn-mind/src/mind.rs | 20 +++++++++++++- tools/eureka-cut10-mutations.psd1 | 44 ++++++++++++++++++++++++++++++ 4 files changed, 96 insertions(+), 1 deletion(-) diff --git a/crates/huginn-daemon/src/daemon.rs b/crates/huginn-daemon/src/daemon.rs index 02d4776..2e3c04f 100644 --- a/crates/huginn-daemon/src/daemon.rs +++ b/crates/huginn-daemon/src/daemon.rs @@ -116,6 +116,10 @@ pub(crate) mod tests { pub(crate) const INSTANCE: &str = "yggdrasil"; pub(crate) const OTHER: &str = "thought-cage"; + /// `INSTANCE`'s own length, differing in its last byte alone. + pub(crate) const NEAR: &str = "yggdrasix"; + /// `INSTANCE` whole, with more after it. + pub(crate) const PREFIXED: &str = "yggdrasil-two"; pub(crate) const CAMPAIGN: &str = "eureka-state"; pub(crate) fn slug(value: &str) -> Slug { @@ -320,6 +324,27 @@ pub(crate) mod tests { let history = HuginnMindRequest::History { instance: slug(OTHER), scope }; assert_eq!(daemon.handle(history, now()), HuginnMindResponse::Refused(foreign)); + // Two names a comparison could mistake for this mind's. `NEAR` is + // `INSTANCE`'s own length and differs in its last byte, so a check + // reading lengths, first bytes, or running only when the declared name + // is longer admits it; `PREFIXED` carries `INSTANCE` whole, so a prefix + // test admits it. Both are refused on the read side and the write side. + assert_eq!(NEAR.len(), INSTANCE.len()); + assert!(PREFIXED.starts_with(INSTANCE)); + for declared in [NEAR, PREFIXED] { + let refusal = MindRefusal::ForeignInstance { declared: declared.into(), mind: INSTANCE.into() }; + let query = HuginnMindRequest::Query { instance: slug(declared), query: PipelineQuery::default() }; + assert_eq!(daemon.handle(query, now()), HuginnMindResponse::Refused(refusal.clone()), "{declared}"); + let view = HuginnMindRequest::View { instance: slug(declared), id: writes[0].clone() }; + assert_eq!(daemon.handle(view, now()), HuginnMindResponse::Refused(refusal.clone()), "{declared}"); + let admit = HuginnMindRequest::Admit(batch(declared, vec![identity(declared)])); + assert_eq!( + daemon.handle(admit, now()), + HuginnMindResponse::Admit(PipelineAdmissionOutcome::Refused(refusal)), + "{declared}" + ); + } + assert_eq!(status(&mut daemon).documents, 1, "nothing landed"); } diff --git a/crates/huginn-mind/src/fixtures.rs b/crates/huginn-mind/src/fixtures.rs index 69b8d97..5797fe0 100644 --- a/crates/huginn-mind/src/fixtures.rs +++ b/crates/huginn-mind/src/fixtures.rs @@ -22,6 +22,14 @@ use crate::store::test_stores::MemoryStore; pub(crate) const INSTANCE: &str = "yggdrasil"; pub(crate) const OTHER_INSTANCE: &str = "thought-cage"; +/// A foreign instance of `INSTANCE`'s own length, differing in its last byte +/// alone: a comparison of lengths, of first bytes, or one gated on the +/// declared name being the longer admits it, and a comparison of values does +/// not. +pub(crate) const NEAR_INSTANCE: &str = "yggdrasix"; +/// A foreign instance carrying `INSTANCE` whole as its prefix, so a prefix +/// test admits it. +pub(crate) const PREFIXED_INSTANCE: &str = "yggdrasil-two"; pub(crate) const CAMPAIGN: &str = "eureka-state"; pub(crate) const REPO: &str = "GameCult/Epiphany"; pub(crate) const OTHER_REPO: &str = "GameCult/Huginn"; diff --git a/crates/huginn-mind/src/mind.rs b/crates/huginn-mind/src/mind.rs index bf40e16..78b54c8 100644 --- a/crates/huginn-mind/src/mind.rs +++ b/crates/huginn-mind/src/mind.rs @@ -405,9 +405,16 @@ mod tests { /// 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. + /// + /// The foreign names are chosen so the check cannot pass by resembling a + /// comparison. `NEAR_INSTANCE` is the mind's own length and shares all but + /// its last byte, so a check reading only the lengths, only the first byte, + /// or running only when the declared name is longer lets it through; + /// `PREFIXED_INSTANCE` has the mind's whole name as its prefix, so a check + /// asking whether one starts with the other lets that one through. #[test] fn require_instance_is_the_one_check_admission_and_the_daemon_share() { - use crate::fixtures::{OTHER_INSTANCE, now, provenance, seeded}; + use crate::fixtures::{NEAR_INSTANCE, OTHER_INSTANCE, PREFIXED_INSTANCE, now, provenance, seeded}; use crate::receipt::Faculty; let mut mind = seeded(); @@ -415,6 +422,17 @@ mod tests { assert_eq!(mind.require_instance(&slug(OTHER_INSTANCE)).err(), Some(foreign.clone())); assert_eq!(mind.require_instance(&slug(INSTANCE)), Ok(())); + assert_eq!(NEAR_INSTANCE.len(), INSTANCE.len(), "the near name is the mind's own length"); + assert_eq!(NEAR_INSTANCE[..INSTANCE.len() - 1], INSTANCE[..INSTANCE.len() - 1], "and differs in one byte"); + assert!(PREFIXED_INSTANCE.starts_with(INSTANCE), "the prefixed name carries the mind's whole name"); + for declared in [NEAR_INSTANCE, PREFIXED_INSTANCE] { + assert_eq!( + mind.require_instance(&slug(declared)).err(), + Some(MindRefusal::ForeignInstance { declared: declared.into(), mind: INSTANCE.into() }), + "{declared} is not {INSTANCE}" + ); + } + let batch = crate::admission::PipelineAdmissionBatch { instance: slug(OTHER_INSTANCE), provenance: provenance(Faculty::Hands), diff --git a/tools/eureka-cut10-mutations.psd1 b/tools/eureka-cut10-mutations.psd1 index 8d22bfa..bd61cf6 100644 --- a/tools/eureka-cut10-mutations.psd1 +++ b/tools/eureka-cut10-mutations.psd1 @@ -50,6 +50,42 @@ } @{ Id = 'D1L' + Rule = 'The instance check compares the names, not their lengths.' + Test = 'mind::tests::require_instance_is_the_one_check_admission_and_the_daemon_share' + Command = 'cargo test -p huginn-mind --lib' + File = 'crates/huginn-mind/src/mind.rs' + Old = ' if declared != self.instance() {' + New = ' if declared.0.len() != self.instance().0.len() {' + } + @{ + Id = 'D1L2' + Rule = 'It compares the names whole, not their first byte.' + Test = 'mind::tests::require_instance_is_the_one_check_admission_and_the_daemon_share' + Command = 'cargo test -p huginn-mind --lib' + File = 'crates/huginn-mind/src/mind.rs' + Old = ' if declared != self.instance() {' + New = ' if declared.0.as_bytes().first() != self.instance().0.as_bytes().first() {' + } + @{ + Id = 'D1L3' + Rule = 'It runs on every declared name, not only on one long enough to look suspicious.' + Test = 'mind::tests::require_instance_is_the_one_check_admission_and_the_daemon_share' + Command = 'cargo test -p huginn-mind --lib' + File = 'crates/huginn-mind/src/mind.rs' + Old = ' if declared != self.instance() {' + New = ' if declared.0.len() > self.instance().0.len() && declared != self.instance() {' + } + @{ + Id = 'D1L4' + Rule = 'It compares the names, not whether one begins with the other.' + Test = 'mind::tests::require_instance_is_the_one_check_admission_and_the_daemon_share' + Command = 'cargo test -p huginn-mind --lib' + File = 'crates/huginn-mind/src/mind.rs' + Old = ' if declared != self.instance() {' + New = ' if !declared.0.starts_with(&self.instance().0) {' + } + @{ + Id = 'D1N' Rule = 'The refusal names which mind refused, so an agent can act on the field rather than guess.' Test = 'mind::tests::require_instance_is_the_one_check_admission_and_the_daemon_share' Command = 'cargo test -p huginn-mind --lib' @@ -76,6 +112,14 @@ Ok(()) '@ } + @{ + Id = 'D1DL' + Rule = 'The same comparison, reached the daemon''s way: a read naming a name of the mind''s own length is refused too.' + Test = 'daemon::tests::a_read_or_a_write_naming_another_instance_is_refused_by_the_mind_and_writes_nothing' + File = 'crates/huginn-mind/src/mind.rs' + Old = ' if declared != self.instance() {' + New = ' if declared.0.len() != self.instance().0.len() {' + } @{ Id = 'D2' Rule = 'Rulings 14 and 18 on the wire: the daemon passes the declared instance to the mind as the client sent it.'