Soul: falsify the pass-2 scope binding, topology admission, and replay

Ten test-only additions, no production source touched.

- mailbox: the end-to-end proof the kernel-level test does not give — a
  controller turn bound at revision N commits through the mailbox after an
  unrelated commit at N+1, and a stamped body still fails closed on a
  foreign scope.
- world: the scope preimage excludes revision, phase, a place declared
  elsewhere, a non-incident route, and another subject's Position, across
  real commits; a forged affordance list cannot widen authority.
- patch: RouteSelfLoop, UnplacedSubject, and NoOperationEffect are each
  reachable; a forged relocate effect dies at apply time on all three
  preconditions; RefKind is not a serializable shape.
- journal: forged store rows carrying a self-loop, an out-of-range cost, a
  non-place endpoint, or a stray position are refused by recover, and a
  pre-topology row type is refused outright.
- controllers: the typed view carries only the actor's own place and the
  routes incident to it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-04 18:08:59 +02:00
commit e99af63f9a
5 changed files with 732 additions and 0 deletions

View file

@ -3648,6 +3648,126 @@ mod tests {
assert!(operational_surface.contains("speaker_subject_id"));
}
/// Soul falsification: the typed view carries the acting subject's own place
/// and the routes incident to it, and no more. Another subject's position
/// and a route that touches neither endpoint stay out of the surface.
#[test]
fn soul_the_typed_view_exposes_only_the_actors_place_and_incident_routes() {
use crate::world::{AccessKind, Cost, EdgeId, EntityId, PlaceSnapshot, RouteSnapshot};
let actor_id = SubjectId::issue();
let other_id = SubjectId::issue();
let actor_controller = ControllerId::issue();
let other_controller = ControllerId::issue();
let speak_affordance = AffordanceId::issue();
// Ordered so the snapshot vectors stay in ID order, as `snapshot` builds
// them.
let mut place_ids = [EntityId::issue(), EntityId::issue(), EntityId::issue()];
place_ids.sort();
let [yard, road, vault] = place_ids;
// `EdgeId` has no test allocator; it is transparent over a UUID, so two
// literals in byte order do the job without touching production source.
let edge = |value: &str| {
serde_json::from_value::<EdgeId>(Value::String(value.into())).expect("an edge ID")
};
let first_edge = edge("11111111-1111-4111-8111-111111111111");
let second_edge = edge("22222222-2222-4222-8222-222222222222");
let opportunity = DecisionOpportunity {
world_id: WorldId::issue(),
revision: 12,
scope_digest: ScopeDigest::fixture("sha256:scope"),
scope: DecisionScope {
subject_id: actor_id,
},
controller_id: actor_controller,
controller_mode: ControllerMode::OperationalAgent,
affordance_ids: vec![speak_affordance],
};
let actor = SubjectSnapshot {
id: actor_id,
label: "The Walker".into(),
kind: SubjectKind::Person,
controller_id: actor_controller,
controller_mode: ControllerMode::OperationalAgent,
human_controller: None,
affordances: BTreeMap::from([(AffordanceKind::Speak, speak_affordance)]),
position: Some(yard),
};
let other = SubjectSnapshot {
id: other_id,
label: "The Vault Keeper".into(),
kind: SubjectKind::Person,
controller_id: other_controller,
controller_mode: ControllerMode::OperationalAgent,
human_controller: None,
affordances: BTreeMap::new(),
position: Some(vault),
};
let named_place = |id, label: &str| PlaceSnapshot {
id,
label: label.into(),
container: None,
};
let named_route = |id, label: &str, from, to| RouteSnapshot {
id,
label: label.into(),
from,
to,
access: AccessKind::Public,
cost: Cost(6),
open: true,
};
let snapshot = WorldSnapshot {
world_id: opportunity.world_id,
revision: opportunity.revision,
phase: WorldPhase::Active,
owner: PrincipalId::new("scope-fixture-owner"),
title: "Kharad".into(),
draft_approvals: BTreeSet::new(),
required_approvers: BTreeSet::new(),
subjects: vec![actor.clone(), other],
places: vec![
named_place(yard, "The Cavity Yard"),
named_place(road, "The Rhythm Road"),
named_place(vault, "The Sealed Vault"),
],
routes: vec![
named_route(first_edge, "The Yard Ramp", yard, road),
named_route(second_edge, "The Vault Stair", road, vault),
],
events: Vec::new(),
opportunities: vec![opportunity.clone()],
state_digest: "sha256:state".into(),
last_commit_digest: None,
};
let selected = SelectedDecision {
snapshot,
subject: actor,
opportunity,
speak_affordance,
};
let view = selected.typed_view().unwrap();
assert!(view.contains("The Cavity Yard"));
assert!(view.contains("The Yard Ramp"));
for leaked in [
"The Sealed Vault",
"The Vault Stair",
"The Vault Keeper",
encoded_id(&vault).unwrap().as_str(),
] {
assert!(!view.contains(leaked), "the typed view leaked `{leaked}`");
}
// A subject standing nowhere gets a null place and no routes at all.
let mut unplaced = selected;
unplaced.subject.position = None;
let view = unplaced.typed_view().unwrap();
assert!(!view.contains("The Cavity Yard"));
assert!(!view.contains("The Yard Ramp"));
}
#[test]
fn controller_tool_schemas_cannot_claim_authority_or_envelopes() {
for definition in interpreter_tools().into_iter().chain(operational_tools()) {

View file

@ -950,4 +950,142 @@ mod tests {
Err(JournalError::Corrupt(_))
));
}
/// Soul falsification: the extended replay shape check is not decoration.
/// A forged store row carrying a self-loop, a cost out of range, a
/// non-place endpoint, or a position on a non-place is refused by
/// `recover`, which is the path `WorldJournal::open` takes.
#[test]
fn soul_forged_topology_store_rows_are_refused_on_recover() {
use crate::world::patch::{AccessKind, Cost, EdgeRecord, MAX_ROUTE_COST};
use crate::world::tests::{admit_topology, auth_principal, owner};
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("world.cc");
let (mut kernel, _) = WorldKernel::create(
&path,
crate::world::tests::creation(CommandId::new(), "Forged Rows"),
&auth_principal(owner()),
)
.unwrap();
let topology = admit_topology(&mut kernel);
let commits: Vec<CultCacheEnvelope> = kernel
.journal
.commits
.values()
.map(|commit| {
envelope(
COMMIT_ROW,
COMMIT_SCHEMA,
commit.command.id().key(),
commit,
)
.unwrap()
})
.collect();
let rows_for = |state: &WorldState, row_type: &str| {
let mut rows = commits.clone();
rows.push(envelope(row_type, STATE_SCHEMA, state.world_id.key(), state).unwrap());
rows
};
// The honest state recovers.
assert!(recover(rows_for(&kernel.state, STATE_ROW), None).is_ok());
let self_loop = {
let mut state = kernel.state.clone();
state.edges.insert(
topology.ramp,
EdgeRecord::Route {
label: "The Yard Ramp".into(),
from: topology.yard,
to: topology.yard,
access: AccessKind::Public,
cost: Cost(12),
open: true,
},
);
state
};
let bad_cost = {
let mut state = kernel.state.clone();
state
.edges
.get_mut(&topology.ramp)
.unwrap()
.set_cost(Cost(MAX_ROUTE_COST + 1));
state
};
let non_place_endpoint = {
let mut state = kernel.state.clone();
state.entities.get_mut(&topology.road).unwrap().kind =
crate::world::EntityKind::Resource;
state
};
let stray_position = {
let mut state = kernel.state.clone();
state.positions.insert(
topology.walker,
Position {
place: EntityId::issue(),
},
);
state
};
for forged in [self_loop, bad_cost, non_place_endpoint, stray_position] {
let error = recover(rows_for(&forged, STATE_ROW), None).unwrap_err();
assert!(
matches!(error, JournalError::Corrupt(_)),
"a forged topology row recovered: {error:?}"
);
}
}
/// Soul falsification: a store written before the topology partitions is
/// refused outright, with no migration adapter in the path.
#[test]
fn soul_a_pre_topology_store_row_is_refused() {
use crate::world::tests::{admit_topology, auth_principal, owner};
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("world.cc");
let (mut kernel, _) = WorldKernel::create(
&path,
crate::world::tests::creation(CommandId::new(), "Old Store"),
&auth_principal(owner()),
)
.unwrap();
admit_topology(&mut kernel);
let mut rows: Vec<CultCacheEnvelope> = kernel
.journal
.commits
.values()
.map(|commit| {
envelope(
"world_commit.foundation.v2",
COMMIT_SCHEMA,
commit.command.id().key(),
commit,
)
.unwrap()
})
.collect();
rows.push(
envelope(
"world_state.foundation.v1",
STATE_SCHEMA,
kernel.state.world_id.key(),
&kernel.state,
)
.unwrap(),
);
let error = recover(rows, None).unwrap_err();
let JournalError::Corrupt(message) = error else {
panic!("expected a corrupt store, got {error:?}");
};
assert!(
message.contains("unadmitted row type"),
"unexpected refusal: {message}"
);
}
}

View file

@ -901,4 +901,156 @@ mod tests {
.await;
assert!(matches!(result, Err(MailboxError::Unavailable)));
}
/// Soul falsification: the envelope revision is stamped inside the owner
/// task, so a controller turn bound at revision N still commits after an
/// unrelated commit moved the world to N+1. This exercises the mailbox
/// path, not the kernel gate a hand-built envelope would reach.
#[tokio::test]
async fn soul_a_bound_opportunity_commits_through_the_mailbox_after_an_unrelated_commit() {
let directory = tempfile::tempdir().unwrap();
let (mailbox, _task) = WorldMailbox::open(directory.path().join("world.cc")).unwrap();
let owner = PrincipalId::new("owner");
let authenticated = authenticated_owner();
let declare = |handle: &str, label: &str, controller: NewController| {
Declaration::Subject(SubjectDeclaration {
handle: DraftHandle::new(handle),
label: label.into(),
kind: SubjectKind::Person,
controller,
affordances: BTreeSet::from([AffordanceKind::Speak]),
position: None,
})
};
let created = mailbox
.create_fixture(
CreateWorld {
id: CommandId::new(),
owner: owner.clone(),
title: "Soul Stamping".into(),
patch: WorldPatch {
declarations: vec![
declare("witness", "The Witness", NewController::NarrativePersona),
declare("council", "The Council", NewController::OperationalAgent),
],
operations: Vec::new(),
evidence: Vec::new(),
},
},
&authenticated,
)
.await
.unwrap();
let mut snapshot = mailbox.snapshot().await.unwrap();
for body in [CommandBody::ApproveDraft, CommandBody::ActivateWorld] {
mailbox
.submit_fixture(
CommandEnvelope {
id: CommandId::new(),
world_id: created.world_id,
expected_revision: snapshot.revision,
caller: CallerId::Principal(owner.clone()),
body,
},
&authenticated,
)
.await
.unwrap();
snapshot = mailbox.snapshot().await.unwrap();
}
let pick = |mode: crate::world::ControllerMode| {
snapshot
.opportunities
.iter()
.find(|value| value.controller_mode == mode)
.expect("an active opportunity")
.clone()
};
let bound = pick(crate::world::ControllerMode::OperationalAgent);
let unrelated = pick(crate::world::ControllerMode::NarrativePersona);
let speak = |opportunity: &DecisionOpportunity, text: &str| DecisionInvocation {
affordance_id: opportunity.affordance_ids[0],
action: crate::world::DecisionAction::Speak { text: text.into() },
};
mailbox
.submit_controller(
CommandId::new(),
&unrelated,
speak(&unrelated, "Somebody else moves."),
)
.await
.unwrap();
let moved = mailbox.snapshot().await.unwrap();
assert_eq!(moved.revision, bound.revision + 1);
let receipt = mailbox
.submit_controller(CommandId::new(), &bound, speak(&bound, "Still mine."))
.await
.expect("a bound turn must not die at the envelope CAS");
assert!(matches!(receipt, SubmitReceipt::Applied(_)));
assert_eq!(
mailbox.snapshot().await.unwrap().revision,
bound.revision + 2
);
}
/// Soul falsification: stamping the revision inside the owner must not open
/// a hole. A stamped body still fails closed when the world derives no such
/// opportunity.
#[tokio::test]
async fn soul_a_stamped_submission_still_fails_closed_on_a_foreign_scope() {
let (_directory, _path, mailbox, _task, authenticated, _input, created) = fixture().await;
let mut snapshot = mailbox.snapshot().await.unwrap();
for body in [CommandBody::ApproveDraft, CommandBody::ActivateWorld] {
mailbox
.submit_fixture(
CommandEnvelope {
id: CommandId::new(),
world_id: created.world_id,
expected_revision: snapshot.revision,
caller: CallerId::Principal(PrincipalId::new("owner")),
body,
},
&authenticated,
)
.await
.unwrap();
snapshot = mailbox.snapshot().await.unwrap();
}
assert_eq!(snapshot.phase, crate::world::WorldPhase::Active);
let forged = DecisionOpportunity {
world_id: WorldId::issue(),
revision: 0,
scope_digest: crate::world::ScopeDigest::fixture("sha256:not-a-scope"),
scope: crate::world::DecisionScope {
subject_id: crate::world::SubjectId::issue(),
},
controller_id: crate::world::ControllerId::issue(),
controller_mode: crate::world::ControllerMode::OperationalAgent,
affordance_ids: vec![crate::world::AffordanceId::issue()],
};
let result = mailbox
.submit_controller(
CommandId::new(),
&forged,
DecisionInvocation {
affordance_id: forged.affordance_ids[0],
action: crate::world::DecisionAction::Speak {
text: "Let me in.".into(),
},
},
)
.await;
// The owner stamps `world_id` as well as `expected_revision`, so the
// envelope's own `WorldMismatch` check is unreachable for these two
// bodies. `exact_opportunity` is what refuses the foreign world.
assert!(
matches!(
result,
Err(MailboxError::Kernel(KernelError::OpportunityMismatch))
),
"a foreign scope was not refused: {result:?}"
);
}
}

View file

@ -2943,4 +2943,177 @@ mod tests {
));
assert_eq!(kernel.snapshot().unwrap(), snapshot);
}
/// Soul falsification: the preimage must exclude the revision, the phase,
/// and every component outside the scope. Each step below is a real commit
/// that moves the world, not a hand-edited state.
#[test]
fn soul_scope_digest_excludes_revision_phase_and_everything_outside_the_scope() {
let dir = tempfile::tempdir().unwrap();
let (mut kernel, _) = WorldKernel::create(
dir.path().join("world.cc"),
creation(CommandId::new(), "Preimage"),
&auth_principal(owner()),
)
.unwrap();
let topology = admit_topology(&mut kernel);
let scope = DecisionScope {
subject_id: topology.walker,
};
let base = scope_digest(&kernel.state, scope).unwrap();
let start_revision = kernel.state.revision;
// A new place elsewhere, a route that touches neither the walker's
// place nor it, and a second placed subject: all outside the scope.
let before = kernel.snapshot().unwrap();
submit_owner(
&mut kernel,
&before,
CommandBody::AdmitPatch {
answers: None,
patch: WorldPatch {
declarations: vec![
Declaration::Entity(EntityDeclaration {
handle: DraftHandle::new("annex"),
label: "The Annex".into(),
kind: EntityKind::Place,
container: None,
}),
Declaration::Route(RouteDeclaration {
handle: DraftHandle::new("annexway"),
label: "The Annex Way".into(),
from: Ref::Existing(topology.road),
to: Ref::Draft(DraftHandle::new("annex")),
access: AccessKind::Public,
cost: Cost(3),
}),
Declaration::Subject(SubjectDeclaration {
handle: DraftHandle::new("runner"),
label: "The Runner".into(),
kind: SubjectKind::Person,
controller: NewController::OperationalAgent,
affordances: BTreeSet::from([AffordanceKind::Speak]),
position: Some(Ref::Existing(topology.road)),
}),
],
operations: Vec::new(),
evidence: Vec::new(),
},
},
);
assert_ne!(kernel.state.revision, start_revision);
assert_eq!(scope_digest(&kernel.state, scope).unwrap(), base);
// Activation changes the phase, the approvals, and the revision.
let active = activate(&mut kernel);
assert_eq!(active.phase, WorldPhase::Active);
assert_eq!(scope_digest(&kernel.state, scope).unwrap(), base);
// Another subject's Position moving is not this scope's business.
let annexway = *kernel
.state
.edges
.iter()
.find(|(_, record)| record.label() == "The Annex Way")
.expect("the declared route")
.0;
let runner = *kernel
.state
.subjects
.iter()
.find(|(_, subject)| subject.label == "The Runner")
.expect("the declared subject")
.0;
let active = kernel.snapshot().unwrap();
submit_owner(
&mut kernel,
&active,
CommandBody::AdmitPatch {
answers: None,
patch: WorldPatch {
declarations: Vec::new(),
operations: vec![ComponentOp::Relocate {
subject: Ref::Existing(runner),
via: Ref::Existing(annexway),
}],
evidence: Vec::new(),
},
},
);
assert_eq!(
kernel.state.positions.get(&runner),
Some(&Position {
place: kernel
.state
.entities
.iter()
.find(|(_, record)| record.label == "The Annex")
.map(|(id, _)| *id)
.unwrap()
})
);
assert_eq!(scope_digest(&kernel.state, scope).unwrap(), base);
// A route incident to the walker's own place is inside the scope.
let moved = kernel.snapshot().unwrap();
submit_owner(
&mut kernel,
&moved,
CommandBody::AdmitPatch {
answers: None,
patch: WorldPatch {
declarations: Vec::new(),
operations: vec![ComponentOp::CloseRoute {
route: Ref::Existing(topology.ramp),
}],
evidence: Vec::new(),
},
},
);
assert_ne!(scope_digest(&kernel.state, scope).unwrap(), base);
}
/// Soul falsification: a claimed opportunity whose `affordance_ids` name
/// grants this scope does not hold cannot widen authority. The kernel reads
/// the derived list, so the forged one is ignored and the invocation is
/// denied on the real grants.
#[test]
fn soul_a_forged_affordance_list_cannot_widen_authority() {
let dir = tempfile::tempdir().unwrap();
let (mut kernel, _) = WorldKernel::create(
dir.path().join("world.cc"),
creation(CommandId::new(), "Affordances"),
&auth_principal(owner()),
)
.unwrap();
let active = activate(&mut kernel);
let persona = opportunity(&active, ControllerMode::NarrativePersona);
let operator = opportunity(&active, ControllerMode::OperationalAgent);
// The persona claims the operator's affordance and lists it as its own.
let mut forged = persona.clone();
forged.affordance_ids = vec![operator.affordance_ids[0], persona.affordance_ids[0]];
let caller = CallerId::Controller(persona.controller_id);
let error = kernel
.submit(
command(
&active,
CommandId::new(),
caller.clone(),
CommandBody::ExerciseDecision {
opportunity: forged,
invocation: DecisionInvocation {
affordance_id: operator.affordance_ids[0],
action: DecisionAction::Speak {
text: "Not my grant.".into(),
},
},
},
),
&AuthenticatedCaller::fixture(caller),
)
.unwrap_err();
assert!(matches!(error, KernelError::AffordanceDenied));
assert_eq!(kernel.snapshot().unwrap(), active);
}
}

View file

@ -2111,4 +2111,153 @@ mod tests {
assert!(matches!(wrong_phase, KernelError::Invariant(_)));
assert_eq!(candidate, kernel.state);
}
/// Soul falsification: `RefKind` is never persisted, and it could not be.
/// `Subject(Option<SubjectKind>)` under `#[serde(tag = "namespace")]` is an
/// internally tagged newtype variant over a non-map, which serde refuses at
/// runtime. The derives are unexercised weight, and the deviation is safe
/// only because nothing writes a `RefKind`.
#[test]
fn soul_ref_kind_is_not_a_serializable_shape() {
assert!(serde_json::to_value(RefKind::Entity(EntityKind::Place)).is_ok());
assert!(serde_json::to_value(ANY_SUBJECT).is_err());
assert!(serde_json::to_value(RefKind::Subject(Some(SubjectKind::Person))).is_err());
}
/// Soul falsification: the three pass-2 `Mismatch` variants that no landed
/// test names are each reachable from a real patch.
#[test]
fn soul_self_loop_unplaced_subject_and_no_effect_are_each_reachable() {
let directory = tempfile::tempdir().unwrap();
let mut kernel = draft_world(directory.path());
let before = kernel.snapshot().unwrap();
// A route whose two endpoints resolve to one place.
let error = kernel
.submit(
command(
&before,
CommandId::new(),
CallerId::Principal(owner()),
admit(patch_of(vec![
entity("hollow", "The Hollow", EntityKind::Place),
route("noose", "The Noose", "hollow", "hollow", 4),
])),
),
&auth_principal(owner()),
)
.unwrap_err();
let KernelError::PatchRejected(mismatches) = error else {
panic!("expected a rejected patch");
};
assert_eq!(
mismatches,
vec![Mismatch::RouteSelfLoop {
referent: draft("noose")
}]
);
let topology = admit_topology(&mut kernel);
let active = activate(&mut kernel);
let commits_before = kernel.journal.commit_count();
let unplaced = *kernel
.state
.subjects
.iter()
.find(|(subject_id, _)| !kernel.state.positions.contains_key(*subject_id))
.expect("a genesis subject was declared without a position")
.0;
// Relocating a subject that stands nowhere.
let error = kernel
.submit(
command(
&active,
CommandId::new(),
CallerId::Principal(owner()),
admit(operations_of(vec![ComponentOp::Relocate {
subject: Ref::Existing(unplaced),
via: Ref::Existing(topology.ramp),
}])),
),
&auth_principal(owner()),
)
.unwrap_err();
let KernelError::PatchRejected(mismatches) = error else {
panic!("expected a rejected patch");
};
assert_eq!(mismatches, vec![Mismatch::UnplacedSubject { operation: 0 }]);
// Three operations that each change nothing: the ramp is already open,
// the shutter already closed, and the ramp already costs twelve.
let error = kernel
.submit(
command(
&active,
CommandId::new(),
CallerId::Principal(owner()),
admit(operations_of(vec![
ComponentOp::OpenRoute {
route: Ref::Existing(topology.ramp),
},
ComponentOp::CloseRoute {
route: Ref::Existing(topology.shutter),
},
ComponentOp::AlterCost {
route: Ref::Existing(topology.ramp),
cost: Cost(12),
},
])),
),
&auth_principal(owner()),
)
.unwrap_err();
let KernelError::PatchRejected(mismatches) = error else {
panic!("expected a rejected patch");
};
assert_eq!(
mismatches,
vec![
Mismatch::NoOperationEffect { operation: 0 },
Mismatch::NoOperationEffect { operation: 1 },
Mismatch::NoOperationEffect { operation: 2 },
]
);
assert_eq!(kernel.snapshot().unwrap(), active);
assert_eq!(kernel.journal.commit_count(), commits_before);
}
/// Soul falsification: `admit_resolved` re-derives every relocate
/// precondition, so a forged effect naming a route the subject does not
/// stand on, a closed route, or a restricted one dies at apply time.
#[test]
fn soul_a_forged_relocate_effect_is_refused_at_apply_time() {
let directory = tempfile::tempdir().unwrap();
let mut kernel = draft_world(directory.path());
let topology = admit_topology(&mut kernel);
activate(&mut kernel);
for edge_id in [topology.span, topology.shutter, topology.toll] {
let forged = super::super::WorldEffect::PatchAdmitted {
resolved: ResolvedPatch {
subjects: Vec::new(),
entities: Vec::new(),
routes: Vec::new(),
operations: vec![ResolvedOp::Relocate {
subject_id: topology.walker,
edge_id,
}],
evidence: Vec::new(),
},
};
let mut candidate = kernel.state.clone();
let error = super::super::apply_effect(
&mut candidate,
&CallerId::Principal(owner()),
&forged,
)
.unwrap_err();
assert!(matches!(error, KernelError::Invariant(_)));
assert_eq!(candidate, kernel.state);
}
}
}