Import key: a subject names the record it came from, and admission keeps the mapping one-to-one

ImportKey (schema, record) rides SubjectDeclaration and SubjectState as an
optional field (serde default, skipped when None, so keyless worlds keep their
digests) and is exposed on SubjectSnapshot. resolve_patch, the one resolution
owner for genesis and the owner, consumer, Play and elaborator doors, refuses a
malformed key, a key declared twice in one patch, and a key any existing subject
holds, retired included. admit_resolved, the subjects map's only writer, holds
the same injectivity as an invariant. Typed mismatches name the handle only.

Compile-checked (cargo check --tests) on Starfire; tests not yet run, so no
claim that this commit's tests pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-10 17:23:26 +02:00
commit b8465e50d3
7 changed files with 698 additions and 0 deletions

View file

@ -518,6 +518,7 @@ mod tests {
},
controller,
position: Some(Ref::Existing(commons)),
import_key: None,
})
};
WorldPatch {
@ -1195,6 +1196,7 @@ mod tests {
controller: NewController::External { consumer },
affordances: BTreeSet::new(),
position: Some(Ref::Existing(commons)),
import_key: None,
})])
};
// A binding to another consumer is outside its ground.
@ -1573,6 +1575,7 @@ mod tests {
controller,
affordances,
position: Some(Ref::Existing(mirror.commons)),
import_key: None,
})])
};
let pairing = |result: Result<SubmitReceipt, KernelError>| {

View file

@ -7258,6 +7258,7 @@ mod tests {
let heard = EntityId::issue();
let unheard = EntityId::issue();
let actor = SubjectSnapshot {
import_key: None,
id: actor_id,
label: "Mara at the rain gate".into(),
kind: SubjectKind::Person,
@ -7304,6 +7305,7 @@ mod tests {
last_acted_at: None,
};
let speaker = SubjectSnapshot {
import_key: None,
id: speaker_id,
label: "Iris in the tollhouse".into(),
kind: SubjectKind::Person,
@ -7427,6 +7429,7 @@ mod tests {
affordance_ids: vec![speak_affordance],
};
let actor = SubjectSnapshot {
import_key: None,
id: actor_id,
label: "The Ledger Clerk".into(),
kind: SubjectKind::Person,
@ -7701,6 +7704,7 @@ mod tests {
controller: NewController::NarrativePersona,
affordances: kernel_speak_grant(),
position: Some(Ref::Draft(DraftHandle::new("commons"))),
import_key: None,
}),
Declaration::Subject(SubjectDeclaration {
handle: DraftHandle::new("mara"),
@ -7709,6 +7713,7 @@ mod tests {
controller: NewController::NarrativePersona,
affordances: kernel_speak_grant(),
position: Some(Ref::Draft(DraftHandle::new("commons"))),
import_key: None,
}),
Declaration::Subject(SubjectDeclaration {
handle: DraftHandle::new("otho"),
@ -7717,6 +7722,7 @@ mod tests {
controller: NewController::NarrativePersona,
affordances: kernel_speak_grant(),
position: Some(Ref::Draft(DraftHandle::new("vault"))),
import_key: None,
}),
],
operations: Vec::new(),
@ -7854,6 +7860,7 @@ mod tests {
controller: NewController::NarrativePersona,
affordances: kernel_speak_grant(),
position: Some(Ref::Draft(DraftHandle::new("commons"))),
import_key: None,
}),
Declaration::Subject(SubjectDeclaration {
handle: DraftHandle::new("human"),
@ -7864,6 +7871,7 @@ mod tests {
},
affordances: kernel_speak_grant(),
position: Some(Ref::Draft(DraftHandle::new("commons"))),
import_key: None,
}),
Declaration::Subject(SubjectDeclaration {
handle: DraftHandle::new("operational"),
@ -7872,6 +7880,7 @@ mod tests {
controller: NewController::OperationalAgent,
affordances: kernel_speak_grant(),
position: Some(Ref::Draft(DraftHandle::new("vault"))),
import_key: None,
}),
],
operations: Vec::new(),
@ -9047,6 +9056,7 @@ mod tests {
affordance_ids: vec![speak_affordance],
};
let subject = SubjectSnapshot {
import_key: None,
id: subject_id,
label: "Mara at the rain gate".into(),
kind: SubjectKind::Person,
@ -9102,6 +9112,7 @@ mod tests {
last_acted_at: None,
};
let speaker = SubjectSnapshot {
import_key: None,
id: speaker_id,
label: "Iris in the tollhouse".into(),
kind: SubjectKind::Person,
@ -9223,6 +9234,7 @@ mod tests {
affordance_ids: vec![speak_affordance],
};
let actor = SubjectSnapshot {
import_key: None,
id: actor_id,
label: "The Walker".into(),
kind: SubjectKind::Person,
@ -9248,6 +9260,7 @@ mod tests {
last_acted_at: None,
};
let other = SubjectSnapshot {
import_key: None,
id: other_id,
label: "The Vault Keeper".into(),
kind: SubjectKind::Person,
@ -9701,6 +9714,7 @@ mod tests {
index: 0,
};
let subject = |id, label: &str, position, components, commitments, pressures| SubjectSnapshot {
import_key: None,
id,
label: label.into(),
kind: SubjectKind::Person,
@ -10825,6 +10839,7 @@ mod tests {
affordances: kernel_speak_grant(),
// Speech needs a room to fill.
position: Some(Ref::Draft(DraftHandle::new("commons"))),
import_key: None,
}),
],
operations: Vec::new(),
@ -11424,6 +11439,7 @@ mod tests {
},
affordances: kernel_speak_grant(),
position: Some(Ref::Draft(DraftHandle::new("roll-hall"))),
import_key: None,
}),
Declaration::Subject(SubjectDeclaration {
handle: DraftHandle::new("watch-officer"),
@ -11432,6 +11448,7 @@ mod tests {
controller: NewController::NarrativePersona,
affordances: kernel_speak_grant(),
position: Some(Ref::Draft(DraftHandle::new("roll-hall"))),
import_key: None,
}),
Declaration::Subject(SubjectDeclaration {
handle: DraftHandle::new("signal-council"),
@ -11440,6 +11457,7 @@ mod tests {
controller: NewController::OperationalAgent,
affordances: kernel_speak_grant(),
position: Some(Ref::Draft(DraftHandle::new("roll-hall"))),
import_key: None,
}),
],
operations: Vec::new(),
@ -11925,6 +11943,7 @@ mod tests {
controller: NewController::NarrativePersona,
affordances: kernel_speak_grant(),
position: Some(Ref::Draft(DraftHandle::new(road_container))),
import_key: None,
}));
let operations = if roots == Roots::None {
Vec::new()
@ -13608,6 +13627,7 @@ mod tests {
controller: NewController::NarrativePersona,
affordances: kernel_speak_grant(),
position,
import_key: None,
})
};
let creation = mailbox
@ -14616,6 +14636,7 @@ mod tests {
controller,
affordances: kernel_speak_grant(),
position: Some(Ref::Draft(DraftHandle::new("commons"))),
import_key: None,
}));
}
let creation = mailbox
@ -15167,6 +15188,7 @@ mod tests {
controller,
affordances: kernel_speak_grant(),
position: Some(Ref::Draft(DraftHandle::new(&format!("room{index}")))),
import_key: None,
}));
}
let creation = mailbox
@ -15647,6 +15669,7 @@ mod tests {
},
affordances: kernel_speak_grant(),
position: Some(Ref::Draft(DraftHandle::new("commons"))),
import_key: None,
}),
],
operations: Vec::new(),
@ -16566,6 +16589,7 @@ mod tests {
},
affordances: kernel_speak_grant(),
position: Some(Ref::Draft(DraftHandle::new("commons"))),
import_key: None,
})))
.collect(),
operations: Vec::new(),
@ -18054,6 +18078,7 @@ mod tests {
controller: NewController::NarrativePersona,
affordances: kernel_speak_grant(),
position: Some(Ref::Draft(DraftHandle::new("commons"))),
import_key: None,
}),
Declaration::Subject(SubjectDeclaration {
handle: DraftHandle::new("subject1"),
@ -18062,6 +18087,7 @@ mod tests {
controller: NewController::OperationalAgent,
affordances: kernel_speak_grant(),
position: Some(Ref::Draft(DraftHandle::new("commons"))),
import_key: None,
}),
];
let creation = mailbox
@ -18562,6 +18588,7 @@ mod tests {
controller,
affordances: kernel_speak_grant(),
position: Some(Ref::Draft(DraftHandle::new("commons"))),
import_key: None,
})
};
let ledger = EvidenceRef::new("the fixture ledger");

View file

@ -0,0 +1,529 @@
//! One record, one subject: admission keeps the import-key mapping injective at
//! every door and at every revision, and a world nobody imported into is
//! byte-for-byte what it was before keys existed.
use crate::patch::{
AccessKind, ComponentOp, Cost, Declaration, DraftHandle, EntityDeclaration, EntityKind,
ImportKey, Mismatch, Ref, RouteDeclaration,
};
use crate::tests::{activate, auth_principal, command, creation, owner};
use crate::{
CallerId, CommandBody, CommandId, ConsumerId, CreateWorld, EntityId, JurisdictionKey,
KernelError, NewController, PatchAnswer, SubjectDeclaration, SubjectKind, SubmitReceipt,
SystemCapability, WorldKernel, WorldPatch,
};
use std::collections::BTreeSet;
fn key(schema: &str, record: &str) -> ImportKey {
ImportKey::new(schema, record)
}
fn alpha() -> ImportKey {
key("aetheria.ship", "Alpha")
}
/// The fixture genesis with its witness imported from `alpha()`.
fn keyed_creation(title: &str) -> CreateWorld {
let mut world = creation(CommandId::new(), title);
for declaration in &mut world.patch.declarations {
if let Declaration::Subject(subject) = declaration
&& subject.handle == DraftHandle::new("persona")
{
subject.import_key = Some(alpha());
}
}
world
}
fn create(world: CreateWorld) -> (tempfile::TempDir, Result<WorldKernel, KernelError>) {
let directory = tempfile::tempdir().unwrap();
let result = WorldKernel::create(
directory.path().join("world.cc"),
world,
&auth_principal(owner()),
)
.map(|(kernel, _)| kernel);
(directory, result)
}
fn keyed_world() -> (tempfile::TempDir, WorldKernel) {
let (dir, kernel) = create(keyed_creation("Keyed"));
(dir, kernel.unwrap())
}
fn rejected(result: Result<impl Sized, KernelError>) -> Vec<Mismatch> {
match result {
Err(KernelError::PatchRejected(set)) => set,
Err(other) => panic!("expected a patch refusal, got {other:?}"),
Ok(_) => panic!("expected a patch refusal, the command was admitted"),
}
}
fn patch_of(declarations: Vec<Declaration>) -> WorldPatch {
WorldPatch {
declarations,
operations: Vec::new(),
evidence: Vec::new(),
}
}
fn submit_as(
kernel: &mut WorldKernel,
caller: CallerId,
answers: Option<PatchAnswer>,
patch: WorldPatch,
) -> Result<SubmitReceipt, KernelError> {
let snapshot = kernel.snapshot().unwrap();
kernel.submit(
command(
&snapshot,
CommandId::new(),
caller.clone(),
CommandBody::AdmitPatch { answers, patch },
),
&crate::AuthenticatedCaller::fixture(caller),
)
}
fn commons(kernel: &WorldKernel) -> EntityId {
*kernel.state.entities.keys().next().unwrap()
}
/// A mirror subject: the one shape every door may declare, since it needs no
/// catalog grant.
fn mirror(handle: &str, import_key: Option<ImportKey>, place: EntityId) -> Declaration {
Declaration::Subject(SubjectDeclaration {
handle: DraftHandle::new(handle),
label: format!("The {handle}"),
kind: SubjectKind::Person,
controller: NewController::External {
consumer: ConsumerId::of_name("import-key"),
},
affordances: BTreeSet::new(),
position: Some(Ref::Existing(place)),
import_key,
})
}
fn held(handle: &str) -> Mismatch {
Mismatch::ImportKeyHeld {
handle: DraftHandle::new(handle),
}
}
fn duplicate(handle: &str) -> Mismatch {
Mismatch::DuplicateImportKey {
handle: DraftHandle::new(handle),
}
}
fn malformed(handle: &str) -> Mismatch {
Mismatch::MalformedImportKey {
handle: DraftHandle::new(handle),
}
}
fn keys_in(kernel: &WorldKernel) -> Vec<ImportKey> {
kernel
.snapshot()
.unwrap()
.subjects
.into_iter()
.filter_map(|subject| subject.import_key)
.collect()
}
#[test]
fn one_patch_cannot_declare_two_subjects_with_one_import_key() {
// At genesis.
let mut world = creation(CommandId::new(), "Twins");
for declaration in &mut world.patch.declarations {
if let Declaration::Subject(subject) = declaration
&& (subject.handle == DraftHandle::new("persona")
|| subject.handle == DraftHandle::new("operator"))
{
subject.import_key = Some(alpha());
}
}
let (_dir, result) = create(world);
assert_eq!(rejected(result), vec![duplicate("operator")]);
// In a later patch, with the key unheld.
let (_dir, mut kernel) = keyed_world();
let place = commons(&kernel);
let revision = kernel.state.revision;
let beta = Some(key("aetheria.ship", "Beta"));
let error = submit_as(
&mut kernel,
CallerId::Principal(owner()),
None,
patch_of(vec![
mirror("first", beta.clone(), place),
mirror("second", beta, place),
]),
);
assert_eq!(rejected(error), vec![duplicate("second")]);
assert_eq!(kernel.state.revision, revision, "a refusal moved the world");
}
/// The fixture world past genesis: the owner adds an unwalked place, and the
/// world is active so Play and the elaborator can act.
fn active_keyed_world() -> (tempfile::TempDir, WorldKernel, EntityId) {
let (dir, mut kernel) = keyed_world();
let place = commons(&kernel);
submit_as(
&mut kernel,
CallerId::Principal(owner()),
None,
patch_of(vec![
Declaration::Entity(EntityDeclaration {
handle: DraftHandle::new("dead-end"),
label: "The Unwalked Road".into(),
kind: EntityKind::Place,
container: None,
}),
Declaration::Route(RouteDeclaration {
handle: DraftHandle::new("gate"),
label: "The Field Gate".into(),
from: Ref::Existing(place),
to: Ref::Draft(DraftHandle::new("dead-end")),
access: AccessKind::Public,
cost: Cost(1),
}),
]),
)
.unwrap();
let dead_end = *kernel
.state
.entities
.iter()
.find(|(_, record)| record.label == "The Unwalked Road")
.map(|(id, _)| id)
.unwrap();
activate(&mut kernel);
(dir, kernel, dead_end)
}
#[test]
fn a_held_import_key_is_refused_at_every_door_after_the_revision_has_moved() {
let (_dir, mut kernel, dead_end) = active_keyed_world();
assert!(kernel.state.revision > 1, "the world has moved past genesis");
let answer = crate::derive_boundaries(&kernel.state)
.unwrap()
.into_iter()
.find(|boundary| {
matches!(boundary, crate::CausalBoundary::UnelaboratedDestination { place, .. }
if *place == dead_end)
})
.expect("the dead end is an unelaborated destination");
let doors: Vec<(&str, CallerId, Option<PatchAnswer>)> = vec![
("owner", CallerId::Principal(owner()), None),
(
"consumer",
CallerId::System(SystemCapability::Consumer {
consumer: ConsumerId::of_name("import-key"),
}),
None,
),
("play", CallerId::System(SystemCapability::Play), None),
(
"elaborator",
CallerId::System(SystemCapability::Elaborator {
jurisdiction: JurisdictionKey::PlaceSubtree(dead_end),
}),
Some(PatchAnswer::Boundary(answer)),
),
];
for (door, caller, answers) in doors {
let revision = kernel.state.revision;
let result = submit_as(
&mut kernel,
caller.clone(),
answers.clone(),
patch_of(vec![mirror("copy", Some(alpha()), dead_end)]),
);
// The owner and the consumer author in Draft only, so in an active
// world those two may refuse for the phase before they resolve; the
// held key is named wherever the declaration is resolved at all.
match result {
Err(KernelError::PatchRejected(set)) => {
assert!(set.contains(&held("copy")), "{door}: {set:?}")
}
other => {
assert!(
matches!(door, "owner" | "consumer"),
"{door} did not resolve the declaration: {other:?}"
);
}
}
assert_eq!(
kernel.state.revision, revision,
"{door} refusal moved the world"
);
}
// The same path admits an unheld key, so the refusals are the key's.
let fresh = key("aetheria.ship", "Gamma");
let receipt = submit_as(
&mut kernel,
CallerId::System(SystemCapability::Play),
None,
patch_of(vec![mirror("fresh", Some(fresh.clone()), dead_end)]),
)
.unwrap();
assert!(matches!(receipt, SubmitReceipt::Applied(_)));
// A later revision holds it too.
let again = submit_as(
&mut kernel,
CallerId::System(SystemCapability::Play),
None,
patch_of(vec![mirror("fresh-again", Some(fresh), dead_end)]),
);
assert_eq!(rejected(again), vec![held("fresh-again")]);
}
#[test]
fn a_held_import_key_is_refused_in_a_later_patch() {
let (_dir, mut kernel) = keyed_world();
let place = commons(&kernel);
let revision = kernel.state.revision;
let error = submit_as(
&mut kernel,
CallerId::Principal(owner()),
None,
patch_of(vec![mirror("again", Some(alpha()), place)]),
);
assert_eq!(rejected(error), vec![held("again")]);
assert_eq!(kernel.state.revision, revision);
assert_eq!(keys_in(&kernel), vec![alpha()]);
}
#[test]
fn a_retired_subjects_import_key_is_never_reissued() {
let (_dir, mut kernel) = keyed_world();
let place = commons(&kernel);
let witness = kernel
.snapshot()
.unwrap()
.subjects
.into_iter()
.find(|subject| subject.import_key.is_some())
.unwrap()
.id;
submit_as(
&mut kernel,
CallerId::Principal(owner()),
None,
WorldPatch {
declarations: Vec::new(),
operations: vec![ComponentOp::Retire {
subject: Ref::Existing(witness),
}],
evidence: Vec::new(),
},
)
.unwrap();
let retired = kernel
.snapshot()
.unwrap()
.subjects
.into_iter()
.find(|subject| subject.id == witness)
.unwrap();
assert!(retired.retired, "the witness is retired");
assert_eq!(
retired.import_key,
Some(alpha()),
"a retired subject keeps its key"
);
let error = submit_as(
&mut kernel,
CallerId::Principal(owner()),
None,
patch_of(vec![mirror("reborn", Some(alpha()), place)]),
);
assert_eq!(rejected(error), vec![held("reborn")]);
}
#[test]
fn keys_that_differ_in_any_way_are_distinct_records() {
let (_dir, mut kernel) = keyed_world();
let place = commons(&kernel);
// Same length, case, prefix, the same concatenation split elsewhere, and
// the parts swapped: none is the held key.
let forgeries = [
key("aetheria.ship", "Alphb"),
key("aetheria.ship", "alpha"),
key("aetheria.ship", "Alph"),
key("aetheria.ship", "Alpha2"),
key("aetheria.shipA", "lpha"),
key("aetheria.sh", "ipAlpha"),
key("Aetheria.ship", "Alpha"),
key("Alpha", "aetheria.ship"),
key("aetheria.ship2", "Alpha"),
];
let declarations = forgeries
.iter()
.enumerate()
.map(|(index, forged)| mirror(&format!("forged-{index}"), Some(forged.clone()), place))
.collect();
submit_as(
&mut kernel,
CallerId::Principal(owner()),
None,
patch_of(declarations),
)
.expect("nine distinct keys beside the held one are admitted together");
let held_now: BTreeSet<ImportKey> = keys_in(&kernel).into_iter().collect();
assert_eq!(held_now.len(), forgeries.len() + 1);
// And each is then held exactly: repeating any one is refused.
for (index, forged) in forgeries.iter().enumerate() {
let again = submit_as(
&mut kernel,
CallerId::Principal(owner()),
None,
patch_of(vec![mirror("repeat", Some(forged.clone()), place)]),
);
assert_eq!(rejected(again), vec![held("repeat")], "forgery {index}");
}
}
#[test]
fn a_malformed_import_key_is_refused_without_echoing_it() {
let (_dir, mut kernel) = keyed_world();
let place = commons(&kernel);
let long = "x".repeat(201);
let wide = "é".repeat(101);
let cases = [
key("", "Delta"),
key("aetheria.ship", ""),
key("aetheria ship", "Delta"),
key("aetheria.ship", "Del\tta"),
key("aetheria.ship", "Del\u{a0}ta"),
key("aetheria.ship", " Delta"),
key("aetheria.ship", "Delta\n"),
key(&long, "Delta"),
key("aetheria.ship", &long),
key("aetheria.ship", &wide),
key(&wide, "Delta"),
];
for (index, bad) in cases.iter().enumerate() {
let set = rejected(submit_as(
&mut kernel,
CallerId::Principal(owner()),
None,
patch_of(vec![mirror("bad", Some(bad.clone()), place)]),
));
assert_eq!(set, vec![malformed("bad")], "case {index}");
let spelled = format!("{set:?}");
assert!(
!spelled.contains("Delta") && !spelled.contains("aetheria"),
"case {index} echoed the key"
);
}
// The limit is 200 bytes per part, counted in bytes.
let edge = "é".repeat(100);
let at_limit = [
key(&"x".repeat(200), &edge),
key("aetheria.ship", &"y".repeat(200)),
];
let declarations = at_limit
.iter()
.enumerate()
.map(|(index, limit)| mirror(&format!("edge-{index}"), Some(limit.clone()), place))
.collect();
submit_as(
&mut kernel,
CallerId::Principal(owner()),
None,
patch_of(declarations),
)
.expect("keys of exactly 200 bytes per part are admitted");
}
#[test]
fn the_subjects_map_refuses_a_second_subject_for_one_key_even_when_resolved_elsewhere() {
let (_dir, kernel) = keyed_world();
let place = commons(&kernel);
let resolve = |handle: &str| {
crate::patch::resolve_patch(
&kernel.state,
CommandId::new(),
&patch_of(vec![mirror(
handle,
Some(key("aetheria.ship", "Omega")),
place,
)]),
None,
None,
)
.expect("the key is unheld at the revision both were resolved against")
};
let (first, second) = (resolve("first"), resolve("second"));
let mut state = kernel.state.clone();
let before = state.subjects.len();
let effect = |resolved| crate::WorldEffect::PatchAdmitted {
answers: None,
resolved,
};
crate::apply_effect(
&mut state,
CommandId::new(),
&CallerId::Principal(owner()),
&effect(first),
)
.unwrap();
assert_eq!(state.subjects.len(), before + 1);
let result = crate::apply_effect(
&mut state,
CommandId::new(),
&CallerId::Principal(owner()),
&effect(second),
);
assert!(
matches!(result, Err(KernelError::Invariant(_))),
"{result:?}"
);
assert_eq!(
state.subjects.len(),
before + 1,
"the second subject was installed"
);
}
#[test]
fn keyless_world_digests_unchanged() {
// A subject nobody imported serialises without the field at all, so every
// digest written before keys existed is the digest of the same bytes.
let (_dir, kernel) = create(creation(CommandId::new(), "Keyless"));
let kernel = kernel.unwrap();
let state = serde_json::to_string(&kernel.state).unwrap();
assert!(!state.contains("import_key"), "a keyless world grew a field");
// A keyed world carries it, round-trips, and digests differently.
let (_dir, keyed) = keyed_world();
let keyed_state = serde_json::to_string(&keyed.state).unwrap();
assert!(keyed_state.contains("import_key"));
let back: crate::WorldState = serde_json::from_str(&keyed_state).unwrap();
assert_eq!(
crate::state_digest(&back).unwrap(),
crate::state_digest(&keyed.state).unwrap()
);
assert_ne!(
crate::state_digest(&keyed.state).unwrap(),
crate::state_digest(&kernel.state).unwrap()
);
// And a pre-key state deserialises with every key absent.
let stripped: crate::WorldState = serde_json::from_str(&state).unwrap();
assert!(stripped.subjects.values().all(|s| s.import_key.is_none()));
}
#[test]
fn a_keyed_world_reopens_with_its_keys_and_digests() {
let (dir, kernel) = keyed_world();
let snapshot = kernel.snapshot().unwrap();
let world_id = snapshot.world_id;
drop(kernel);
let reopened = WorldKernel::open(dir.path().join("world.cc"), world_id).unwrap();
let again = reopened.snapshot().unwrap();
assert_eq!(again.state_digest, snapshot.state_digest);
assert_eq!(keys_in(&reopened), vec![alpha()]);
}

View file

@ -1107,6 +1107,7 @@ mod tests {
},
affordances: kernel_speak_grant(),
position: None,
import_key: None,
}),
scale_intent: WorldScaleIntentRef::default(),
grammar: None,
@ -1191,6 +1192,7 @@ mod tests {
},
affordances: kernel_speak_grant(),
position: None,
import_key: None,
}),
scale_intent: WorldScaleIntentRef::default(),
grammar: None,
@ -1222,6 +1224,7 @@ mod tests {
controller: NewController::OperationalAgent,
affordances: BTreeSet::from([speak_entry(&kernel)]),
position: Some(Ref::Draft(DraftHandle::new("rhythm-road"))),
import_key: None,
}),
],
operations: Vec::new(),
@ -1757,6 +1760,7 @@ mod tests {
controller: NewController::Human { principal: owner },
affordances: kernel_speak_grant(),
position: None,
import_key: None,
}),
scale_intent: WorldScaleIntentRef::default(),
grammar: None,

View file

@ -62,6 +62,8 @@ mod controllers;
mod cover;
mod elaboration;
mod grammar;
#[cfg(test)]
mod import_key_tests;
mod journal;
mod lens;
mod local_inference;
@ -118,6 +120,7 @@ pub(crate) use patch::{
PersonaMaterial, Position, PressureMagnitude, PressureSource, Quantity, Reach,
SubjectDeclaration, WorldScaleIntent, WorldScaleIntentRef,
};
pub use patch::ImportKey;
#[cfg(test)]
use patch::{
AffordanceDeclaration, AudienceRef, AudienceSpec, ChannelDeclaration, ComponentOp,
@ -718,6 +721,10 @@ pub enum CommandBody {
struct SubjectState {
label: String,
kind: SubjectKind,
/// Written only when a subject declaration is resolved; absent from a
/// subject nobody imported, so its digests are unchanged.
#[serde(default, skip_serializing_if = "Option::is_none")]
import_key: Option<ImportKey>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
@ -1007,6 +1014,8 @@ pub struct SubjectSnapshot {
pub id: SubjectId,
pub label: String,
pub kind: SubjectKind,
/// The Aetheria record this subject was imported from, if it was.
pub import_key: Option<ImportKey>,
/// Absent for an externally controlled mirror, which has no controller and
/// no turn. Every reader fails closed on `None`; none gets a default.
pub(crate) controller_id: Option<ControllerId>,
@ -2168,6 +2177,19 @@ fn admit_resolved(
"admitted controller ID collision".into(),
));
}
// The subjects map's only writer holds the same injectivity the
// resolver checks, so no resolved effect can install a second
// subject for one record.
if subject.subject.import_key.is_some()
&& state
.subjects
.values()
.any(|held| held.import_key == subject.subject.import_key)
{
return Err(KernelError::Invariant(
"admitted subject repeats a held import key".into(),
));
}
if state
.subjects
.insert(subject.subject_id, subject.subject.clone())
@ -4006,6 +4028,7 @@ fn snapshot(state: &WorldState) -> Result<WorldSnapshot, KernelError> {
id: *subject_id,
label: subject.label.clone(),
kind: subject.kind,
import_key: subject.import_key.clone(),
controller_id: controller.id(),
controller_mode: controller.mode(),
human_controller: controller.human_principal().cloned(),
@ -5486,6 +5509,7 @@ mod tests {
// The fixture world is one room. Co-located speech needs a place to
// fill, so every fixture subject stands in the commons.
position: Some(Ref::Draft(DraftHandle::new(COMMONS))),
import_key: None,
})
}
@ -5588,6 +5612,7 @@ mod tests {
.chain(std::iter::once(Ref::Draft(DraftHandle::new("convene"))))
.collect(),
position: Some(Ref::Draft(DraftHandle::new(COMMONS))),
import_key: None,
}),
],
},
@ -5738,6 +5763,7 @@ mod tests {
controller: NewController::OperationalAgent,
affordances: BTreeSet::from([speak]),
position: Some(Ref::Draft(DraftHandle::new("yard"))),
import_key: None,
}),
],
// A route that should start closed is declared and closed in one
@ -5843,6 +5869,7 @@ mod tests {
}))
.collect(),
position: Some(Ref::Existing(place)),
import_key: None,
})
}
@ -6363,6 +6390,7 @@ mod tests {
controller: NewController::OperationalAgent,
affordances: civic_grants(handles, &speak),
position: Some(place),
import_key: None,
})
};
let declarations = civic_affordances()
@ -6419,6 +6447,7 @@ mod tests {
&speak,
),
position: Some(Ref::Draft(DraftHandle::new("hall"))),
import_key: None,
}),
Declaration::Subject(SubjectDeclaration {
handle: DraftHandle::new("reeve"),
@ -6430,6 +6459,7 @@ mod tests {
&speak,
),
position: Some(Ref::Draft(DraftHandle::new("chamber"))),
import_key: None,
}),
person(
"farmer",
@ -6453,6 +6483,7 @@ mod tests {
controller: NewController::NarrativePersona,
affordances: civic_grants(&["levy", "petition"], &speak),
position: None,
import_key: None,
}),
])
.collect();
@ -6616,6 +6647,7 @@ mod tests {
Ref::Draft(DraftHandle::new("recant")),
]),
position: place.map(|place| Ref::Draft(DraftHandle::new(place))),
import_key: None,
})
};
submit_owner(
@ -7884,6 +7916,7 @@ mod tests {
controller: NewController::OperationalAgent,
affordances: BTreeSet::from([speak.clone()]),
position: Some(Ref::Existing(topology.road)),
import_key: None,
}),
],
operations: Vec::new(),
@ -8902,6 +8935,7 @@ mod custody_tests {
controller: NewController::OperationalAgent,
affordances: BTreeSet::from([speak.clone()]),
position: Some(Ref::Existing(topology.yard)),
import_key: None,
}),
],
operations: vec![ComponentOp::Admit {
@ -9561,6 +9595,7 @@ mod custody_tests {
controller: NewController::OperationalAgent,
affordances: BTreeSet::from([speak.clone()]),
position: Some(Ref::Existing(topology.yard)),
import_key: None,
}),
],
operations: vec![admit(u64::MAX), admit(1)],
@ -11553,6 +11588,7 @@ mod witness_tests {
controller: NewController::NarrativePersona,
affordances: BTreeSet::from([speak.clone(), Ref::Draft(DraftHandle::new("beacon"))]),
position: Some(Ref::Draft(DraftHandle::new(place))),
import_key: None,
})
}
@ -12666,6 +12702,7 @@ mod witness_tests {
// Every non-mirror subject must hold one.
affordances: BTreeSet::from([Ref::Existing(beacon)]),
position: position.map(Ref::Existing),
import_key: None,
}),
],
operations: vec![witness(
@ -13030,6 +13067,7 @@ mod clock_tests {
Ref::Draft(DraftHandle::new("threaten")),
]),
position: Some(Ref::Draft(DraftHandle::new(place))),
import_key: None,
})
};
let subject = |handle: &str| Ref::Draft(DraftHandle::new(handle));
@ -17083,6 +17121,7 @@ mod clock_tests {
controller: NewController::NarrativePersona,
affordances: BTreeSet::from([speak]),
position: Some(Ref::Existing(clockwork.dead_end)),
import_key: None,
}),
],
operations: Vec::new(),
@ -17149,6 +17188,7 @@ mod clock_tests {
controller: NewController::NarrativePersona,
affordances: BTreeSet::from([speak]),
position: Some(Ref::Existing(clockwork.dead_end)),
import_key: None,
}),
],
operations: Vec::new(),
@ -17226,6 +17266,7 @@ mod clock_tests {
},
affordances: BTreeSet::new(),
position: Some(Ref::Existing(commons)),
import_key: None,
})],
operations: Vec::new(),
evidence: Vec::new(),
@ -17319,6 +17360,7 @@ mod clock_tests {
controller: NewController::NarrativePersona,
affordances: BTreeSet::from([speak]),
position: Some(Ref::Existing(clockwork.dead_end)),
import_key: None,
}),
],
operations: vec![ComponentOp::Mint {
@ -17561,6 +17603,7 @@ mod clock_tests {
// the boundary its own answer claims: an occupied
// place is no longer unelaborated.
position: Some(Ref::Existing(clockwork.dead_end)),
import_key: None,
})],
operations: Vec::new(),
evidence: Vec::new(),
@ -17739,6 +17782,7 @@ mod clock_tests {
controller: NewController::NarrativePersona,
affordances: BTreeSet::from([speak]),
position: Some(Ref::Existing(clockwork.dead_end)),
import_key: None,
})],
operations: vec![ComponentOp::Retire {
subject: Ref::Draft(DraftHandle::new("stray")),
@ -17848,6 +17892,7 @@ mod clock_tests {
controller: NewController::NarrativePersona,
affordances: BTreeSet::from([speak]),
position: Some(Ref::Existing(commons)),
import_key: None,
}),
],
operations: vec![ComponentOp::GrantAffordance {
@ -18062,6 +18107,7 @@ mod clock_tests {
controller: NewController::NarrativePersona,
affordances: BTreeSet::from([speak]),
position: Some(Ref::Existing(clockwork.dead_end)),
import_key: None,
})],
operations: vec![ComponentOp::GrantAffordance {
subject: Ref::Draft(DraftHandle::new("stray")),
@ -18480,6 +18526,7 @@ mod clock_tests {
Ref::Existing(clockwork.threaten),
]),
position: Some(Ref::Existing(clockwork.dead_end)),
import_key: None,
})],
operations: vec![ComponentOp::RevokeAffordance {
subject: Ref::Draft(DraftHandle::new("stray")),
@ -18928,6 +18975,7 @@ mod clock_tests {
controller: NewController::NarrativePersona,
affordances: BTreeSet::from([speak.clone()]),
position: None,
import_key: None,
}),
Declaration::Channel(ChannelDeclaration {
handle: DraftHandle::new("wire"),

View file

@ -121,6 +121,7 @@ impl WorldMailbox {
controller,
affordances: kernel_speak_grant(),
position: Some(Ref::Draft(DraftHandle::new(GENESIS_PLACE))),
import_key: None,
})
};
// Roots stand beside the commons, not inside it: a root contained by
@ -1011,6 +1012,7 @@ mod tests {
},
affordances: kernel_speak_grant(),
position: Some(Ref::Draft(DraftHandle::new(GENESIS_PLACE))),
import_key: None,
}),
],
operations: Vec::new(),
@ -1398,6 +1400,7 @@ mod tests {
controller,
affordances: kernel_speak_grant(),
position: Some(Ref::Draft(DraftHandle::new(format!("room-{handle}")))),
import_key: None,
})
};
let room = |handle: &str| {

View file

@ -945,6 +945,45 @@ impl EvidenceRef {
}
}
/// The Aetheria record a subject was imported from: the CultCache schema id and
/// the record key, compared exactly as two separate texts so no pair of
/// distinct records can spell one key. Admission keeps the mapping injective;
/// a key is never matched by prefix, case or normalisation.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
#[serde(deny_unknown_fields)]
pub struct ImportKey {
schema: String,
record: String,
}
/// The longest either part of an import key may be, in bytes.
const MAX_IMPORT_KEY_PART_BYTES: usize = 200;
impl ImportKey {
pub fn new(schema: impl Into<String>, record: impl Into<String>) -> Self {
Self {
schema: schema.into(),
record: record.into(),
}
}
pub fn schema(&self) -> &str {
&self.schema
}
pub fn record(&self) -> &str {
&self.record
}
fn is_canonical(&self) -> bool {
[&self.schema, &self.record].into_iter().all(|part| {
!part.is_empty()
&& part.len() <= MAX_IMPORT_KEY_PART_BYTES
&& !part.chars().any(char::is_whitespace)
})
}
}
/// `position` is the subject's presence: one place it stands in. A subject
/// declared without one is unplaced until a later pass gives placement an owner.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
@ -959,6 +998,11 @@ pub(crate) struct SubjectDeclaration {
/// atomically.
pub(crate) affordances: BTreeSet<Ref<AffordanceId>>,
pub(crate) position: Option<Ref<EntityId>>,
/// The record this subject was imported from, if any. Absent from a
/// subject nobody imported, so such a world's state and digests are what
/// they were before import keys existed.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) import_key: Option<ImportKey>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
@ -1365,6 +1409,19 @@ pub enum Mismatch {
SpeakRenderPathDisagreesWithGrammar {
handle: DraftHandle,
},
/// An import key is empty, has whitespace, or is longer than 200 bytes in
/// either part.
MalformedImportKey {
handle: DraftHandle,
},
/// Two subjects in one patch declare the same import key.
DuplicateImportKey {
handle: DraftHandle,
},
/// A subject already in the world, retired or not, holds this import key.
ImportKeyHeld {
handle: DraftHandle,
},
UnresolvedDraft {
site: Site,
referent: DraftHandle,
@ -3135,6 +3192,14 @@ pub(super) fn resolve_patch(
kind_names.insert(kernel_speak_entry().kind);
}
// Derived from the subjects the world holds, retired ones included, so a
// key is never reissued; no stored index.
let mut import_keys: BTreeSet<&ImportKey> = state
.subjects
.values()
.filter_map(|subject| subject.import_key.as_ref())
.collect();
let mut held_keys = import_keys.clone();
for (position, declaration) in patch.declarations.iter().enumerate() {
let (handle, label, kind) = match declaration {
Declaration::Subject(subject) => (
@ -3174,6 +3239,18 @@ pub(super) fn resolve_patch(
}),
}
}
if let Declaration::Subject(subject) = declaration
&& let Some(key) = &subject.import_key
{
let handle = subject.handle.clone();
if !key.is_canonical() {
mismatches.push(Mismatch::MalformedImportKey { handle });
} else if held_keys.contains(key) {
mismatches.push(Mismatch::ImportKeyHeld { handle });
} else if !import_keys.insert(key) {
mismatches.push(Mismatch::DuplicateImportKey { handle });
}
}
if let Declaration::Affordance(affordance) = declaration {
validate_affordance(
&affordance.handle,
@ -5278,6 +5355,7 @@ pub(super) fn resolve_patch(
subject: SubjectState {
label: input.label.clone(),
kind: input.kind,
import_key: input.import_key.clone(),
},
controller,
affordances: granted,
@ -7902,6 +7980,7 @@ mod tests {
controller: NewController::OperationalAgent,
affordances: BTreeSet::from([speak_entry(kernel)]),
position,
import_key: None,
})
}
@ -8169,6 +8248,7 @@ mod tests {
controller: NewController::NarrativePersona,
affordances: BTreeSet::from([Ref::Draft(DraftHandle::new(handle))]),
position: None,
import_key: None,
})
};
@ -8469,6 +8549,7 @@ mod tests {
},
affordances: BTreeSet::from([speak_entry(&kernel)]),
position: None,
import_key: None,
}),
]);
let error = kernel
@ -8521,6 +8602,7 @@ mod tests {
controller: NewController::NarrativePersona,
affordances: BTreeSet::from([speak_entry(&kernel)]),
position: None,
import_key: None,
}),
]);
let receipt = submit_owner(&mut kernel, &before, admit(repaired));
@ -9076,6 +9158,7 @@ mod tests {
controller: NewController::NarrativePersona,
affordances: BTreeSet::from([speak_entry(&kernel)]),
position: None,
import_key: None,
})]);
submit_owner(&mut kernel, &before, admit(patch.clone()));
let admitted = *kernel
@ -9352,6 +9435,7 @@ mod tests {
controller: NewController::NarrativePersona,
affordances: BTreeSet::from([speak]),
position: None,
import_key: None,
})])),
);
let active = activate(&mut kernel);