Put lens weights in the world (plan step 15, L1 Cut 2)

Lens weights are world data (L1-Q2), replaced only by an owner command
(L1-Q3), and required at creation with no library default (L1-Q6).

Library:
- WorldState.lens_weights, written by genesis through
  ResolvedPatch.lens_weights (Some on the genesis lane only) and carried
  on CreateWorld and CreateWorldIntent (required).
- CommandBody::SetLensWeights and WorldEffect::LensWeightsReplaced: owner
  only, any phase; reduce decides and apply_effect re-decides owner and
  draw rule. A set that never draws is Mismatch::LensWeightsNeverDraw at
  genesis and in the command; verify_state_shape refuses a stored one.
- WorldSnapshot.lens_weights.
- State and commit schemas go to consumer.v5; v4 stores are refused.
- resolve_patch takes the weights; the action lane passes None.

Dungeon:
- CreatePayload.lens_weights required; world_create.v4 in the descriptor
  and operation_schema; the eighth create binding and a lens weights
  control defaulted to uniform_lens_weights(), which is Dungeon policy.
- The smoke harness states uniform weights. The stale-payload test now
  denies v3, a v4 payload without weights (as a payload error) and one
  without a brief; new tests deny an unknown lens and an all-zero set.

Tests: genesis writes and replays weights; an all-zero genesis is refused
and writes no file; only the owner replaces weights (non-owner, stranger,
elaborator, clock, consumer, stale revision, all-zero, hand-built effects);
weights enter state_digest and no scope digest; a history with a weight
change replays to the same digest; forged weight rows fail replay; a v4
store and an all-zero state row are refused; external non-owner refusal
and unknown-lens decode.

This commit builds (Windows only).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-16 15:00:45 +02:00
commit d06fd7ec2e
10 changed files with 815 additions and 33 deletions

View file

@ -44,9 +44,9 @@ pub(crate) struct EveRouteHint {
pub(crate) transport: Option<String>, pub(crate) transport: Option<String>,
} }
/// The `world_create.v3` payload, in one place, so the button that captures it /// The `world_create.v4` payload, in one place, so the button that captures it
/// and the descriptor that advertises it cannot drift. /// and the descriptor that advertises it cannot drift.
const CREATE_BINDINGS: [&str; 7] = [ const CREATE_BINDINGS: [&str; 8] = [
"title", "title",
"brief", "brief",
"subject_label", "subject_label",
@ -54,6 +54,7 @@ const CREATE_BINDINGS: [&str; 7] = [
"operational_agent_label", "operational_agent_label",
"targets", "targets",
"jurisdictions", "jurisdictions",
"lens_weights",
]; ];
const SEED_BINDINGS: [&str; 2] = ["vault_scope", "brief"]; const SEED_BINDINGS: [&str; 2] = ["vault_scope", "brief"];
@ -256,6 +257,11 @@ pub(crate) fn authenticated_surface(
match snapshot { match snapshot {
None => { None => {
// Dungeon policy, editable before submission; the library holds no
// default weights.
let default_lens_weights =
serde_json::to_string(&crate::runtime::uniform_lens_weights())
.context("the default lens weights encode")?;
children.extend([ children.extend([
json!({ json!({
"id":"world.create.title", "id":"world.create.title",
@ -299,6 +305,13 @@ pub(crate) fn authenticated_surface(
"stateBindings":[local_draft("jurisdictions", "json")], "stateBindings":[local_draft("jurisdictions", "json")],
"children":[] "children":[]
}), }),
json!({
"id":"world.create.lens_weights",
"kind":"control.input.textarea",
"props":{"label":"Lens weights","rows":2,"value":default_lens_weights,"placeholder":default_lens_weights},
"stateBindings":[local_draft("lens_weights", "json")],
"children":[]
}),
command_button( command_button(
"world.create", "world.create",
"Create world", "Create world",
@ -309,7 +322,7 @@ pub(crate) fn authenticated_surface(
]); ]);
commands.push(command_descriptor( commands.push(command_descriptor(
"world.create", "world.create",
"ghostlight.world_create.v3", "ghostlight.world_create.v4",
&CREATE_BINDINGS, &CREATE_BINDINGS,
"WorldMailbox", "WorldMailbox",
)); ));
@ -611,7 +624,7 @@ pub(crate) fn operation_schema(operation: &str) -> Option<&'static str> {
"heimdall.auth.begin" => "heimdall.auth_begin_command.v1", "heimdall.auth.begin" => "heimdall.auth_begin_command.v1",
"heimdall.auth.complete" => "heimdall.auth_complete_command.v1", "heimdall.auth.complete" => "heimdall.auth_complete_command.v1",
"app.auth.logout" => "ghostlight.app_logout.v2", "app.auth.logout" => "ghostlight.app_logout.v2",
"world.create" => "ghostlight.world_create.v3", "world.create" => "ghostlight.world_create.v4",
"world.approve" => "ghostlight.world_approve.v0", "world.approve" => "ghostlight.world_approve.v0",
"world.activate" => "ghostlight.world_activate.v0", "world.activate" => "ghostlight.world_activate.v0",
"world.advance_time" => "ghostlight.world_advance_time.v0", "world.advance_time" => "ghostlight.world_advance_time.v0",
@ -758,6 +771,27 @@ mod tests {
assert!(!encoded.contains("session_zero")); assert!(!encoded.contains("session_zero"));
assert!(!encoded.contains("campaign")); assert!(!encoded.contains("campaign"));
assert_eq!(surface["version"], 0); assert_eq!(surface["version"], 0);
// The lens control's default is Dungeon's uniform policy, every stock
// lens named, and it decodes as the weights the payload will carry.
fn find<'a>(node: &'a Value, id: &str) -> Option<&'a Value> {
if node["id"] == id {
return Some(node);
}
node.as_object()?
.values()
.flat_map(|value| match value {
Value::Array(items) => items.iter().collect::<Vec<_>>(),
other => vec![other],
})
.find_map(|child| find(child, id))
}
let control = find(&surface, "world.create.lens_weights").expect("a lens weights control");
let default = control["props"]["value"].as_str().expect("a default value");
let decoded: ghostlight::LensWeights = serde_json::from_str(default).unwrap();
assert_eq!(decoded, crate::runtime::uniform_lens_weights());
assert_eq!(decoded.iter().count(), ghostlight::Lens::ALL.len());
assert!(decoded.iter().all(|(_, weight)| weight == 1));
} }
#[test] #[test]

View file

@ -15,7 +15,7 @@ use ghostlight::{
ConsumerPort, ConsumerRegistry, ControllerError, ControllerModels, ControllerPendingReason, ConsumerPort, ConsumerRegistry, ControllerError, ControllerModels, ControllerPendingReason,
ControllerRunner, ControllerWorkCustody, Cover, CoverBudget, CreateJurisdictionIntent, ControllerRunner, ControllerWorkCustody, Cover, CoverBudget, CreateJurisdictionIntent,
CreateWorldIntent, DEFAULT_SDK_MODEL_PREFIX, DecisionInvocation, DecisionOpportunity, CreateWorldIntent, DEFAULT_SDK_MODEL_PREFIX, DecisionInvocation, DecisionOpportunity,
KernelError, MailboxError, NarrativeRun, OperationalRun, PrincipalCommandIntent, PrincipalId, KernelError, Lens, LensWeights, MailboxError, NarrativeRun, OperationalRun, PrincipalCommandIntent, PrincipalId,
SdkBinding, SeedOutcome, SeedPort, Statement, SubjectId, SubjectKind, SubmissionDisposition, SdkBinding, SeedOutcome, SeedPort, Statement, SubjectId, SubjectKind, SubmissionDisposition,
SubmitReceipt, TickMinutes, VaultEvidenceSource, VerifiedPrincipalEvidence, WorldMailbox, SubmitReceipt, TickMinutes, VaultEvidenceSource, VerifiedPrincipalEvidence, WorldMailbox,
WorldPhase, WorldSnapshot, derive_cover, open_controller_work, open_inference, WorldPhase, WorldSnapshot, derive_cover, open_controller_work, open_inference,
@ -159,7 +159,7 @@ struct CreatePayload {
title: String, title: String,
/// The world's premise in the owner's words, projected to every lane as /// The world's premise in the owner's words, projected to every lane as
/// guidance. Required, may be empty; a payload that omits it is refused, /// guidance. Required, may be empty; a payload that omits it is refused,
/// which is what `world_create.v3` means. /// which is what `world_create.v4` means.
brief: String, brief: String,
subject_label: String, subject_label: String,
#[serde(default)] #[serde(default)]
@ -169,13 +169,17 @@ struct CreatePayload {
/// World-wide target of goal-bearing subjects per kind. Required, and may /// World-wide target of goal-bearing subjects per kind. Required, and may
/// be empty: a world with no target is a deliberate choice, not a default /// be empty: a world with no target is a deliberate choice, not a default
/// that arrives because nobody said anything. A payload that omits it is /// that arrives because nobody said anything. A payload that omits it is
/// refused, which is what `world_create.v3` means. /// refused, which is what `world_create.v4` means.
targets: BTreeMap<SubjectKind, u32>, targets: BTreeMap<SubjectKind, u32>,
/// The jurisdiction roots, declared by genesis beside the commons because /// The jurisdiction roots, declared by genesis beside the commons because
/// `resolve_patch` only resolves roots the same patch declares. A duplicate /// `resolve_patch` only resolves roots the same patch declares. A duplicate
/// handle and a permille sum over 1000 are refused by the resolver, not /// handle and a permille sum over 1000 are refused by the resolver, not
/// pre-checked here: a pre-check would be a second reducer. /// pre-checked here: a pre-check would be a second reducer.
jurisdictions: Vec<CreateJurisdiction>, jurisdictions: Vec<CreateJurisdiction>,
/// Required, and must draw: a payload that omits it is refused, which is
/// what `world_create.v4` means. An unknown lens name is refused by
/// deserialization before any handler runs.
lens_weights: LensWeights,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -1471,6 +1475,7 @@ async fn execute_world(
permille: root.permille, permille: root.permille,
}) })
.collect(), .collect(),
lens_weights: payload.lens_weights,
}, },
verified_principal, verified_principal,
) )
@ -1860,6 +1865,12 @@ fn configured_cover_budget() -> anyhow::Result<CoverBudget> {
.context("cover budget configuration is not usable") .context("cover budget configuration is not usable")
} }
/// Dungeon policy for a world created before Session Zero's sliders exist
/// (D2). Not a library default; the library requires the weights to be stated.
pub(crate) fn uniform_lens_weights() -> LensWeights {
LensWeights::new(Lens::ALL.into_iter().map(|lens| (lens, 1)).collect())
}
/// Sized to the connector's per-caller quota. `Capacity` and `InFlight` /// Sized to the connector's per-caller quota. `Capacity` and `InFlight`
/// refusals are already retryable faults, so a pool above that quota degrades /// refusals are already retryable faults, so a pool above that quota degrades
/// to retry rather than to corruption. /// to retry rather than to corruption.
@ -2883,14 +2894,15 @@ mod tests {
&fixture.cookie, &fixture.cookie,
invocation( invocation(
"world.create", "world.create",
"ghostlight.world_create.v3", "ghostlight.world_create.v4",
0, 0,
json!({ json!({
"title":"Cutover World", "title":"Cutover World",
"brief":"", "brief":"",
"subject_label":"Operator", "subject_label":"Operator",
"targets":{}, "targets":{},
"jurisdictions":[] "jurisdictions":[],
"lens_weights":{"patina":1,"charter":1,"ledger":1,"hearth":1,"tangle":1,"veil":1,"ember":1,"numen":1}
}), }),
&uuid::Uuid::new_v4().to_string(), &uuid::Uuid::new_v4().to_string(),
), ),
@ -2964,14 +2976,15 @@ mod tests {
let id = uuid::Uuid::new_v4().to_string(); let id = uuid::Uuid::new_v4().to_string();
let command = invocation( let command = invocation(
"world.create", "world.create",
"ghostlight.world_create.v3", "ghostlight.world_create.v4",
0, 0,
json!({ json!({
"title":"Retry World", "title":"Retry World",
"brief":"", "brief":"",
"subject_label":"Operator", "subject_label":"Operator",
"targets":{}, "targets":{},
"jurisdictions":[] "jurisdictions":[],
"lens_weights":{"patina":1,"charter":1,"ledger":1,"hearth":1,"tangle":1,"veil":1,"ember":1,"numen":1}
}), }),
&id, &id,
); );
@ -3147,6 +3160,7 @@ mod tests {
operational_agent_label: Some("Operational Agent".into()), operational_agent_label: Some("Operational Agent".into()),
targets, targets,
jurisdictions, jurisdictions,
lens_weights: uniform_lens_weights(),
}, },
&principal, &principal,
) )
@ -4003,10 +4017,10 @@ mod tests {
// ---- The seed command ------------------------------------------------ // ---- The seed command ------------------------------------------------
/// Spec test 1. `world_create.v2` is not kept alive beside v3: an /// Spec test 1. The previous create schema is not kept alive beside
/// invocation announcing it dies at validation before any handler runs, and /// `world_create.v4`: a complete payload announcing it dies at validation before any handler runs,
/// a payload that announces v3 but omits the brief or the scale target is a /// and a payload that announces v4 but omits the lens weights or the brief
/// payload error rather than a defaulted intent. Neither creates a world. /// is a payload error rather than a defaulted intent. None creates a world.
#[tokio::test] #[tokio::test]
async fn a_stale_create_payload_is_refused() { async fn a_stale_create_payload_is_refused() {
let fixture = fixture().await; let fixture = fixture().await;
@ -4015,9 +4029,9 @@ mod tests {
&fixture.cookie, &fixture.cookie,
invocation( invocation(
"world.create", "world.create",
"ghostlight.world_create.v2", "ghostlight.world_create.v3",
0, 0,
json!({"title":"Stale World","subject_label":"Operator","targets":{},"jurisdictions":[]}), json!({"title":"Stale World","brief":"","subject_label":"Operator","targets":{},"jurisdictions":[]}),
&uuid::Uuid::new_v4().to_string(), &uuid::Uuid::new_v4().to_string(),
), ),
) )
@ -4025,14 +4039,39 @@ mod tests {
assert_eq!(stale["state"], "denied"); assert_eq!(stale["state"], "denied");
assert!(current_world(&fixture.state).await.unwrap().is_none()); assert!(current_world(&fixture.state).await.unwrap().is_none());
let unweighted = post(
&fixture.state,
&fixture.cookie,
invocation(
"world.create",
"ghostlight.world_create.v4",
0,
json!({"title":"Unweighted World","brief":"","subject_label":"Operator","targets":{},"jurisdictions":[]}),
&uuid::Uuid::new_v4().to_string(),
),
)
.await;
assert_eq!(unweighted["state"], "denied");
// Refused as a payload, before genesis: a defaulted empty set would
// also be denied, by the resolver, and must not pass for this.
let message = unweighted["message"].as_str().unwrap_or_default();
assert!(
message.contains("missing field `lens_weights`"),
"the omission was not refused at the payload: {message}"
);
assert!(
current_world(&fixture.state).await.unwrap().is_none(),
"a v4 payload with no lens weights created a world anyway"
);
let partial = post( let partial = post(
&fixture.state, &fixture.state,
&fixture.cookie, &fixture.cookie,
invocation( invocation(
"world.create", "world.create",
"ghostlight.world_create.v3", "ghostlight.world_create.v4",
0, 0,
json!({"title":"Half World","subject_label":"Operator","targets":{},"jurisdictions":[]}), json!({"title":"Half World","subject_label":"Operator","targets":{},"jurisdictions":[],"lens_weights":{"patina":1,"charter":1,"ledger":1,"hearth":1,"tangle":1,"veil":1,"ember":1,"numen":1}}),
&uuid::Uuid::new_v4().to_string(), &uuid::Uuid::new_v4().to_string(),
), ),
) )
@ -4040,10 +4079,62 @@ mod tests {
assert_eq!(partial["state"], "denied"); assert_eq!(partial["state"], "denied");
assert!( assert!(
current_world(&fixture.state).await.unwrap().is_none(), current_world(&fixture.state).await.unwrap().is_none(),
"a v2 payload with no scale target created a world anyway" "a v4 payload with no brief created a world anyway"
); );
} }
/// A lens name the library does not know, beside one it does, is refused
/// by deserialization before any handler runs. No world is created.
#[tokio::test]
async fn a_create_payload_naming_an_unknown_lens_is_refused() {
let fixture = fixture().await;
let result = post(
&fixture.state,
&fixture.cookie,
invocation(
"world.create",
"ghostlight.world_create.v4",
0,
json!({"title":"Tribunal World","brief":"","subject_label":"Operator","targets":{},"jurisdictions":[],"lens_weights":{"patina":1,"tribunal":1}}),
&uuid::Uuid::new_v4().to_string(),
),
)
.await;
assert_eq!(result["state"], "denied");
let message = result["message"].as_str().unwrap_or_default();
assert!(
message.contains("tribunal"),
"the unknown lens was not refused by name: {message}"
);
assert!(current_world(&fixture.state).await.unwrap().is_none());
}
/// Every lens named, every weight zero: the payload decodes, and the
/// library's genesis refuses it with `LensWeightsNeverDraw`. No world.
#[tokio::test]
async fn a_create_payload_whose_lenses_never_draw_is_refused() {
let fixture = fixture().await;
let result = post(
&fixture.state,
&fixture.cookie,
invocation(
"world.create",
"ghostlight.world_create.v4",
0,
json!({"title":"Still World","brief":"","subject_label":"Operator","targets":{},"jurisdictions":[],"lens_weights":{"patina":0,"charter":0,"ledger":0,"hearth":0,"tangle":0,"veil":0,"ember":0,"numen":0}}),
&uuid::Uuid::new_v4().to_string(),
),
)
.await;
assert_eq!(result["state"], "denied");
let message = result["message"].as_str().unwrap_or_default();
assert!(
message.contains("LensWeightsNeverDraw"),
"the refusal did not name the draw rule: {message}"
);
assert!(current_world(&fixture.state).await.unwrap().is_none());
}
/// Every `control.button` in a surface, at whatever depth. /// Every `control.button` in a surface, at whatever depth.
fn surface_buttons(node: &Value, into: &mut Vec<String>) { fn surface_buttons(node: &Value, into: &mut Vec<String>) {
if node["kind"] == "control.button" if node["kind"] == "control.button"

View file

@ -172,6 +172,7 @@ pub(super) fn exercise(
evidence: Vec::new(), evidence: Vec::new(),
}, },
None, None,
None,
) )
.map_err(KernelError::PatchRejected)? .map_err(KernelError::PatchRejected)?
.operations .operations

View file

@ -1219,7 +1219,7 @@ mod tests {
}], }],
evidence: vec![EvidenceRef::new(MIRROR_EVIDENCE)], evidence: vec![EvidenceRef::new(MIRROR_EVIDENCE)],
}; };
let resolved = patch::resolve_patch(&kernel.state, command_id, &foreign, None) let resolved = patch::resolve_patch(&kernel.state, command_id, &foreign, None, None)
.expect("the patch resolves"); .expect("the patch resolves");
let effect = super::super::WorldEffect::PatchAdmitted { let effect = super::super::WorldEffect::PatchAdmitted {
answers: None, answers: None,
@ -2234,7 +2234,7 @@ mod tests {
assert!(super::super::journal::verify_state_shape(&previous).is_err()); assert!(super::super::journal::verify_state_shape(&previous).is_err());
assert_eq!( assert_eq!(
super::super::STATE_SCHEMA, super::super::STATE_SCHEMA,
"ghostlight.world_state.consumer.v4" "ghostlight.world_state.consumer.v5"
); );
} }

View file

@ -6667,6 +6667,7 @@ mod tests {
material: None, material: None,
}; };
let snapshot = WorldSnapshot { let snapshot = WorldSnapshot {
lens_weights: crate::tests::stock_weights(),
world_id: opportunity.world_id, world_id: opportunity.world_id,
revision: opportunity.revision, revision: opportunity.revision,
phase: WorldPhase::Active, phase: WorldPhase::Active,
@ -6924,6 +6925,7 @@ mod tests {
open: true, open: true,
}; };
let snapshot = WorldSnapshot { let snapshot = WorldSnapshot {
lens_weights: crate::tests::stock_weights(),
world_id: opportunity.world_id, world_id: opportunity.world_id,
revision: opportunity.revision, revision: opportunity.revision,
phase: WorldPhase::Active, phase: WorldPhase::Active,
@ -7419,6 +7421,7 @@ mod tests {
affordance_ids: vec![speak_affordance], affordance_ids: vec![speak_affordance],
}; };
let snapshot = WorldSnapshot { let snapshot = WorldSnapshot {
lens_weights: crate::tests::stock_weights(),
world_id: opportunity.world_id, world_id: opportunity.world_id,
revision: opportunity.revision, revision: opportunity.revision,
phase: WorldPhase::Active, phase: WorldPhase::Active,
@ -8415,6 +8418,7 @@ mod tests {
let creation = mailbox let creation = mailbox
.create_fixture( .create_fixture(
CreateWorld { CreateWorld {
lens_weights: crate::tests::stock_weights(),
id: CommandId::new(), id: CommandId::new(),
owner: owner.clone(), owner: owner.clone(),
title: "Controller Fixture".into(), title: "Controller Fixture".into(),
@ -9010,6 +9014,7 @@ mod tests {
let creation = mailbox let creation = mailbox
.create_fixture( .create_fixture(
CreateWorld { CreateWorld {
lens_weights: crate::tests::stock_weights(),
id: CommandId::new(), id: CommandId::new(),
owner: owner.clone(), owner: owner.clone(),
title: "Midnight Roll Call".into(), title: "Midnight Roll Call".into(),
@ -9413,6 +9418,7 @@ mod tests {
let creation = mailbox let creation = mailbox
.create_fixture( .create_fixture(
CreateWorld { CreateWorld {
lens_weights: crate::tests::stock_weights(),
id: CommandId::new(), id: CommandId::new(),
owner: owner.clone(), owner: owner.clone(),
title: "Elaboration Fixture".into(), title: "Elaboration Fixture".into(),
@ -9703,6 +9709,7 @@ mod tests {
let creation = mailbox let creation = mailbox
.create_fixture( .create_fixture(
CreateWorld { CreateWorld {
lens_weights: crate::tests::stock_weights(),
id: CommandId::new(), id: CommandId::new(),
owner: owner.clone(), owner: owner.clone(),
title: "Cover Fixture".into(), title: "Cover Fixture".into(),
@ -10250,6 +10257,7 @@ mod tests {
let creation = mailbox let creation = mailbox
.create_fixture( .create_fixture(
CreateWorld { CreateWorld {
lens_weights: crate::tests::stock_weights(),
id: CommandId::new(), id: CommandId::new(),
owner: owner.clone(), owner: owner.clone(),
title: "Apart Fixture".into(), title: "Apart Fixture".into(),
@ -10695,6 +10703,7 @@ mod tests {
mailbox mailbox
.create_fixture( .create_fixture(
CreateWorld { CreateWorld {
lens_weights: crate::tests::stock_weights(),
id: CommandId::new(), id: CommandId::new(),
owner: owner.clone(), owner: owner.clone(),
title: "Seed Fixture".into(), title: "Seed Fixture".into(),
@ -11447,6 +11456,7 @@ mod tests {
mailbox mailbox
.create( .create(
CreateWorldIntent { CreateWorldIntent {
lens_weights: crate::tests::stock_weights(),
id: CommandId::new(), id: CommandId::new(),
title: "The Whole Road".into(), title: "The Whole Road".into(),
brief: String::new(), brief: String::new(),
@ -11607,6 +11617,7 @@ mod tests {
let owner = PrincipalId::new("seed-owner"); let owner = PrincipalId::new("seed-owner");
let authenticated = AuthenticatedCaller::fixture(CallerId::Principal(owner.clone())); let authenticated = AuthenticatedCaller::fixture(CallerId::Principal(owner.clone()));
let genesis = |intent: WorldScaleIntentRef, roots: &[&str]| CreateWorld { let genesis = |intent: WorldScaleIntentRef, roots: &[&str]| CreateWorld {
lens_weights: crate::tests::stock_weights(),
id: CommandId::new(), id: CommandId::new(),
owner: owner.clone(), owner: owner.clone(),
title: "Two Roots".into(), title: "Two Roots".into(),
@ -13135,6 +13146,7 @@ mod tests {
let creation = mailbox let creation = mailbox
.create_fixture( .create_fixture(
CreateWorld { CreateWorld {
lens_weights: crate::tests::stock_weights(),
id: CommandId::new(), id: CommandId::new(),
owner: owner.clone(), owner: owner.clone(),
title: "Witness Fixture".into(), title: "Witness Fixture".into(),
@ -13634,6 +13646,7 @@ mod tests {
let creation = mailbox let creation = mailbox
.create_fixture( .create_fixture(
CreateWorld { CreateWorld {
lens_weights: crate::tests::stock_weights(),
id: CommandId::new(), id: CommandId::new(),
owner: owner.clone(), owner: owner.clone(),
title: "Interruption Fixture".into(), title: "Interruption Fixture".into(),

View file

@ -736,6 +736,11 @@ pub(super) fn verify_state_shape(state: &WorldState) -> Result<(), JournalError>
"the scale intent names a non-place root or distributes more than the whole".into(), "the scale intent names a non-place root or distributes more than the whole".into(),
)); ));
} }
// The third statement of the draw rule, beside genesis and
// `SetLensWeights`: no stored world holds weights that never draw.
if !state.lens_weights.draws() {
return Err(JournalError::Corrupt("the lens weights never draw".into()));
}
// The civic subgraph, in the slot the deleted `authority_scope` check // The civic subgraph, in the slot the deleted `authority_scope` check
// occupied: a jurisdiction names live ground under a canonical kind, an // occupied: a jurisdiction names live ground under a canonical kind, an
// office sits on an institution and lends something to a person who holds // office sits on an institution and lends something to a person who holds
@ -1062,6 +1067,7 @@ mod tests {
let owner = PrincipalId::new("owner@example.test"); let owner = PrincipalId::new("owner@example.test");
let authenticated = AuthenticatedCaller::fixture(CallerId::Principal(owner.clone())); let authenticated = AuthenticatedCaller::fixture(CallerId::Principal(owner.clone()));
let creation = CreateWorld { let creation = CreateWorld {
lens_weights: crate::tests::stock_weights(),
id: CommandId::new(), id: CommandId::new(),
owner: owner.clone(), owner: owner.clone(),
title: "Before".into(), title: "Before".into(),
@ -1144,6 +1150,7 @@ mod tests {
let owner = PrincipalId::new("owner@example.test"); let owner = PrincipalId::new("owner@example.test");
let authenticated = AuthenticatedCaller::fixture(CallerId::Principal(owner.clone())); let authenticated = AuthenticatedCaller::fixture(CallerId::Principal(owner.clone()));
let creation = CreateWorld { let creation = CreateWorld {
lens_weights: crate::tests::stock_weights(),
id: CommandId::new(), id: CommandId::new(),
owner: owner.clone(), owner: owner.clone(),
title: "Kharad".into(), title: "Kharad".into(),
@ -1265,6 +1272,186 @@ mod tests {
} }
} }
/// Lens weights entered both rows — the state row holds them and a commit
/// row carries `LensWeightsReplaced` — so both schemas bumped and both
/// `consumer.v4` strings are refused at open. No migration, no dual read.
#[test]
fn a_store_written_before_the_lens_schema_is_refused() {
for (row_type, schema) in [
(STATE_ROW, "ghostlight.world_state.consumer.v4"),
(COMMIT_ROW, "ghostlight.world_commit.consumer.v4"),
] {
let row = CultCacheEnvelope {
key: "state".into(),
r#type: row_type.into(),
payload: Vec::new(),
stored_at: Utc::now().to_rfc3339(),
schema_id: Some(schema.into()),
};
let error = recover(vec![row], None).unwrap_err();
let JournalError::Corrupt(message) = error else {
panic!("expected a corrupt store for {schema}");
};
assert!(
message.contains("has the wrong schema"),
"unexpected refusal for {schema}: {message}"
);
}
}
/// A world whose history includes an owner's `SetLensWeights`, reopened
/// from its own store rows.
fn lens_history() -> (tempfile::TempDir, WorldKernel, crate::LensWeights) {
use crate::tests::{auth_principal, creation, owner, submit_owner};
use crate::{Lens, LensWeights};
let directory = tempfile::tempdir().unwrap();
let (mut kernel, _) = WorldKernel::create(
directory.path().join("world.cc"),
creation(CommandId::new(), "Lens History"),
&auth_principal(owner()),
)
.unwrap();
let replaced = LensWeights::new(BTreeMap::from([
(Lens::Patina, 0),
(Lens::Charter, 4),
(Lens::Ledger, 1),
(Lens::Numen, 2),
]));
let snapshot = kernel.snapshot().unwrap();
submit_owner(
&mut kernel,
&snapshot,
CommandBody::SetLensWeights {
weights: replaced.clone(),
},
);
(directory, kernel, replaced)
}
fn rows_of(state: &WorldState, commits: &[WorldCommit]) -> Vec<CultCacheEnvelope> {
let mut rows: Vec<CultCacheEnvelope> = commits
.iter()
.map(|commit| {
envelope(COMMIT_ROW, COMMIT_SCHEMA, commit.command.id().key(), commit).unwrap()
})
.collect();
rows.push(envelope(STATE_ROW, STATE_SCHEMA, state.world_id.key(), state).unwrap());
rows
}
/// Replay re-derives the owner's weight change: the honest history reopens
/// to the same state digest and weights, and the store on disk does too.
#[test]
fn a_history_with_a_lens_weight_change_replays_exactly() {
let (directory, kernel, replaced) = lens_history();
let honest: Vec<WorldCommit> = kernel.journal.commits.values().cloned().collect();
let (_, replayed, commits) =
recover(rows_of(&kernel.state, &honest), None).expect("the honest history replays");
assert_eq!(commits.len(), honest.len());
assert_eq!(replayed.state_digest, kernel.state.state_digest);
assert_eq!(replayed.lens_weights, replaced);
let (world_id, digest) = (kernel.state.world_id, kernel.state.state_digest.clone());
drop(kernel);
let reopened = WorldKernel::open(directory.path().join("world.cc"), world_id).unwrap();
assert_eq!(reopened.state.state_digest, digest);
assert_eq!(reopened.state.lens_weights, replaced);
}
/// A stored weight change is not believed about who made it or what it
/// wrote. Each forgery re-seals its row, so the chain digest cannot answer
/// for the authority or draw check.
#[test]
fn a_forged_lens_weights_row_fails_replay() {
use crate::tests::human_principal;
use crate::{Lens, LensWeights};
let (_directory, kernel, _) = lens_history();
let honest: Vec<WorldCommit> = kernel.journal.commits.values().cloned().collect();
let forge = |rewrite: &dyn Fn(&mut WorldCommit)| {
let mut forged_one = false;
let forged: Vec<WorldCommit> = honest
.iter()
.cloned()
.map(|mut commit| {
if matches!(commit.effect, WorldEffect::LensWeightsReplaced { .. }) {
rewrite(&mut commit);
commit.digest = commit_digest(&commit).unwrap();
forged_one = true;
}
commit
})
.collect();
assert!(forged_one, "no lens weights row was found to forge");
let error = recover(rows_of(&kernel.state, &forged), None)
.expect_err("a forged lens weights row replayed");
let JournalError::Corrupt(detail) = &error else {
panic!("unexpected refusal: {error:?}");
};
assert!(
!detail.contains("commit chain is not contiguous"),
"the chain digest answered for the check: {detail}"
);
};
// A non-owner principal with a live approver role in this world.
forge(&|commit| {
if let CommittedCommand::WorldCommand(command) = &mut commit.command {
command.caller = CallerId::Principal(human_principal());
}
});
// The clock capability.
forge(&|commit| {
if let CommittedCommand::WorldCommand(command) = &mut commit.command {
command.caller = CallerId::System(crate::SystemCapability::Clock);
}
});
// Every lens named, all at zero, in both the command and its effect.
let never = LensWeights::new(Lens::ALL.into_iter().map(|lens| (lens, 0)).collect());
forge(&|commit| {
if let CommittedCommand::WorldCommand(command) = &mut commit.command {
command.body = CommandBody::SetLensWeights {
weights: never.clone(),
};
}
commit.effect = WorldEffect::LensWeightsReplaced {
weights: never.clone(),
};
});
}
/// The third statement of the draw rule: a state row whose weights never
/// draw is refused by the shape check itself, not only by replay.
#[test]
fn a_state_row_whose_lens_weights_never_draw_is_refused() {
use crate::{Lens, LensWeights};
let (_directory, kernel, _) = lens_history();
let mut forged = kernel.state.clone();
forged.lens_weights =
LensWeights::new(Lens::ALL.into_iter().map(|lens| (lens, 0)).collect());
let JournalError::Corrupt(detail) =
verify_state_shape(&forged).expect_err("an all-zero weight set passed the shape check")
else {
unreachable!("the shape check only reports corruption")
};
assert!(detail.contains("lens weights"), "unexpected refusal: {detail}");
forged.lens_weights = LensWeights::default();
assert!(verify_state_shape(&forged).is_err());
assert!(verify_state_shape(&kernel.state).is_ok());
let commits: Vec<WorldCommit> = kernel.journal.commits.values().cloned().collect();
let mut zeroed = kernel.state.clone();
zeroed.lens_weights =
LensWeights::new(Lens::ALL.into_iter().map(|lens| (lens, 0)).collect());
assert!(matches!(
recover(rows_of(&zeroed, &commits), None),
Err(JournalError::Corrupt(_))
));
}
/// The tick is re-derived at replay by the same function that produced it, /// The tick is re-derived at replay by the same function that produced it,
/// so a committed motion rewritten after the fact no longer reduces from /// so a committed motion rewritten after the fact no longer reduces from
/// its command. The clock chain refuses a rewritten reading the same way. /// its command. The clock chain refuses a rewritten reading the same way.
@ -1495,6 +1682,7 @@ mod tests {
let authenticated = AuthenticatedCaller::fixture(CallerId::Principal(owner.clone())); let authenticated = AuthenticatedCaller::fixture(CallerId::Principal(owner.clone()));
let creation_id = CommandId::new(); let creation_id = CommandId::new();
let creation = CreateWorld { let creation = CreateWorld {
lens_weights: crate::tests::stock_weights(),
id: creation_id, id: creation_id,
owner: owner.clone(), owner: owner.clone(),
title: "Admitted".into(), title: "Admitted".into(),

View file

@ -171,8 +171,8 @@ impl VerifiedPrincipalEvidence {
} }
} }
pub const STATE_SCHEMA: &str = "ghostlight.world_state.consumer.v4"; pub const STATE_SCHEMA: &str = "ghostlight.world_state.consumer.v5";
pub(crate) const COMMIT_SCHEMA: &str = "ghostlight.world_commit.consumer.v4"; pub(crate) const COMMIT_SCHEMA: &str = "ghostlight.world_commit.consumer.v5";
/// Compatibility tag derived from [`STATE_SCHEMA`]: the trailing /// Compatibility tag derived from [`STATE_SCHEMA`]: the trailing
/// `<family>-<version>` pair (e.g. `foundation-v1`). Callers that publish a /// `<family>-<version>` pair (e.g. `foundation-v1`). Callers that publish a
@ -410,6 +410,9 @@ struct CreateWorld {
/// Authored once, here, and never mutated: `CommandBody::AdmitPatch` carries /// Authored once, here, and never mutated: `CommandBody::AdmitPatch` carries
/// no intent, so write-once is the command shape rather than a check. /// no intent, so write-once is the command shape rather than a check.
scale_intent: WorldScaleIntentRef, scale_intent: WorldScaleIntentRef,
/// The world's flavor as genesis writes it. `SetLensWeights` may replace
/// it later; this command carries what the world began with.
lens_weights: LensWeights,
} }
/// Unattributed creation intent. World ingress derives ownership, controller /// Unattributed creation intent. World ingress derives ownership, controller
@ -431,6 +434,9 @@ pub struct CreateWorldIntent {
/// The jurisdiction roots genesis declares beside the commons, because /// The jurisdiction roots genesis declares beside the commons, because
/// `resolve_patch` only resolves roots the same patch declares. /// `resolve_patch` only resolves roots the same patch declares.
pub jurisdictions: Vec<CreateJurisdictionIntent>, pub jurisdictions: Vec<CreateJurisdictionIntent>,
/// Required. The library holds no default weights; a consumer states the
/// world's flavor or is refused.
pub lens_weights: LensWeights,
} }
/// One jurisdiction root as ingress states it. The handle is a string here and /// One jurisdiction root as ingress states it. The handle is a string here and
@ -672,6 +678,14 @@ pub enum CommandBody {
AdvanceTime { AdvanceTime {
minutes: TickMinutes, minutes: TickMinutes,
}, },
/// Replaces the world's whole lens weight set. Owner only, in any phase,
/// under genesis admission: a set that never draws is refused, and a zero
/// weight is admitted and never draws. Identical weights are still a
/// commit, because the owner's act is canonical. A session that already
/// drew keeps its recorded lens.
SetLensWeights {
weights: LensWeights,
},
} }
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
@ -845,6 +859,10 @@ struct WorldState {
now: FictionalMinutes, now: FictionalMinutes,
/// The authored scale target, written once by genesis and never mutated. /// The authored scale target, written once by genesis and never mutated.
scale_intent: WorldScaleIntent, scale_intent: WorldScaleIntent,
/// Written by genesis and replaced only by `SetLensWeights`; read by the
/// elaborator's draw and by nothing that admits. It enters `state_digest`
/// and no `ScopePreimage`.
lens_weights: LensWeights,
controller_assignments: BTreeMap<DecisionScope, ControllerAssignment>, controller_assignments: BTreeMap<DecisionScope, ControllerAssignment>,
/// What an affordance *is*. World-authored, Draft-only, written by /// What an affordance *is*. World-authored, Draft-only, written by
/// `admit_resolved` alone. /// `admit_resolved` alone.
@ -896,6 +914,11 @@ enum WorldEffect {
to: FictionalMinutes, to: FictionalMinutes,
motion: Motion, motion: Motion,
}, },
/// The owner replaced the world's lens weights. `apply_effect` re-decides
/// the owner and the draw rule.
LensWeightsReplaced {
weights: LensWeights,
},
} }
/// The exact raw command admitted by the world owner. This is the sole /// The exact raw command admitted by the world owner. This is the sole
@ -1113,6 +1136,7 @@ pub struct WorldSnapshot {
/// commitment gazetteer. /// commitment gazetteer.
pub boundaries: Vec<CausalBoundary>, pub boundaries: Vec<CausalBoundary>,
pub scale_deficit: Vec<ScaleDeficitRow>, pub scale_deficit: Vec<ScaleDeficitRow>,
pub lens_weights: LensWeights,
pub state_digest: String, pub state_digest: String,
pub last_commit_digest: Option<String>, pub last_commit_digest: Option<String>,
} }
@ -1318,6 +1342,7 @@ fn prepare_creation(
input.id, input.id,
&input.patch, &input.patch,
Some(&input.scale_intent), Some(&input.scale_intent),
Some(&input.lens_weights),
) )
.map_err(KernelError::PatchRejected)?; .map_err(KernelError::PatchRejected)?;
Ok(PreparedCreation { Ok(PreparedCreation {
@ -1640,7 +1665,7 @@ fn reduce(state: &WorldState, command: &CommandEnvelope) -> Result<WorldEffect,
!(patch.declarations.is_empty() && patch.evidence.is_empty()), !(patch.declarations.is_empty() && patch.evidence.is_empty()),
)?; )?;
let confinement = require_patch_author(state, &command.caller, answers.as_ref())?; let confinement = require_patch_author(state, &command.caller, answers.as_ref())?;
let resolved = patch::resolve_patch(state, command.id, patch, None) let resolved = patch::resolve_patch(state, command.id, patch, None, None)
.map_err(KernelError::PatchRejected)?; .map_err(KernelError::PatchRejected)?;
if let Some(ground) = confinement { if let Some(ground) = confinement {
confine_to_ground(state, &resolved, ground).map_err(KernelError::PatchRejected)?; confine_to_ground(state, &resolved, ground).map_err(KernelError::PatchRejected)?;
@ -1650,6 +1675,17 @@ fn reduce(state: &WorldState, command: &CommandEnvelope) -> Result<WorldEffect,
resolved, resolved,
}) })
} }
CommandBody::SetLensWeights { weights } => {
require_owner(state, &command.caller)?;
if !weights.draws() {
return Err(KernelError::PatchRejected(vec![
Mismatch::LensWeightsNeverDraw,
]));
}
Ok(WorldEffect::LensWeightsReplaced {
weights: weights.clone(),
})
}
} }
} }
@ -1770,6 +1806,9 @@ impl WorldState {
last_opportunity_at: BTreeMap::new(), last_opportunity_at: BTreeMap::new(),
now: FictionalMinutes::default(), now: FictionalMinutes::default(),
scale_intent: WorldScaleIntent::default(), scale_intent: WorldScaleIntent::default(),
// Never persisted: genesis overwrites it, and `verify_state_shape`
// refuses a stored set that never draws.
lens_weights: LensWeights::default(),
controller_assignments: BTreeMap::new(), controller_assignments: BTreeMap::new(),
affordance_catalog: BTreeMap::new(), affordance_catalog: BTreeMap::new(),
affordance_grants: BTreeMap::new(), affordance_grants: BTreeMap::new(),
@ -1812,6 +1851,7 @@ impl WorldState {
command.id, command.id,
&command.patch, &command.patch,
Some(&command.scale_intent), Some(&command.scale_intent),
Some(&command.lens_weights),
) )
.map_err(KernelError::PatchRejected)?; .map_err(KernelError::PatchRejected)?;
if &expected != resolved { if &expected != resolved {
@ -2047,6 +2087,9 @@ fn admit_resolved(state: &mut WorldState, resolved: &ResolvedPatch) -> Result<()
if let Some(intent) = &resolved.scale_intent { if let Some(intent) = &resolved.scale_intent {
state.scale_intent = intent.clone(); state.scale_intent = intent.clone();
} }
if let Some(weights) = &resolved.lens_weights {
state.lens_weights = weights.clone();
}
apply_operations(state, &resolved.operations, &resolved.evidence) apply_operations(state, &resolved.operations, &resolved.evidence)
} }
@ -3718,6 +3761,7 @@ fn snapshot(state: &WorldState) -> Result<WorldSnapshot, KernelError> {
now: state.now, now: state.now,
boundaries: derive_boundaries(state)?, boundaries: derive_boundaries(state)?,
scale_deficit: derive_scale_deficit(state)?, scale_deficit: derive_scale_deficit(state)?,
lens_weights: state.lens_weights.clone(),
state_digest: state.state_digest.clone(), state_digest: state.state_digest.clone(),
last_commit_digest: state.last_commit_digest.clone(), last_commit_digest: state.last_commit_digest.clone(),
}) })
@ -4892,6 +4936,14 @@ fn apply_effect(
.insert(written.source, written.magnitude); .insert(written.source, written.magnitude);
} }
} }
WorldEffect::LensWeightsReplaced { weights } => {
if caller != &CallerId::Principal(state.owner.clone()) || !weights.draws() {
return Err(KernelError::Invariant(
"lens weights effect does not satisfy owner admission".into(),
));
}
state.lens_weights = weights.clone();
}
} }
Ok(()) Ok(())
} }
@ -5064,6 +5116,7 @@ mod tests {
], ],
}, },
scale_intent: WorldScaleIntentRef::default(), scale_intent: WorldScaleIntentRef::default(),
lens_weights: stock_weights(),
} }
} }
@ -8246,6 +8299,7 @@ mod custody_tests {
let mut candidate = kernel.state.clone(); let mut candidate = kernel.state.clone();
let forged = WorldEffect::PatchAdmitted { let forged = WorldEffect::PatchAdmitted {
resolved: ResolvedPatch { resolved: ResolvedPatch {
lens_weights: None,
subjects: Vec::new(), subjects: Vec::new(),
entities: Vec::new(), entities: Vec::new(),
routes: Vec::new(), routes: Vec::new(),
@ -8840,6 +8894,7 @@ mod custody_tests {
let forge = |operations: Vec<ResolvedOp>| WorldEffect::PatchAdmitted { let forge = |operations: Vec<ResolvedOp>| WorldEffect::PatchAdmitted {
resolved: ResolvedPatch { resolved: ResolvedPatch {
lens_weights: None,
subjects: Vec::new(), subjects: Vec::new(),
entities: Vec::new(), entities: Vec::new(),
routes: Vec::new(), routes: Vec::new(),
@ -11493,6 +11548,7 @@ mod witness_tests {
evidence: Vec::new(), evidence: Vec::new(),
}, },
None, None,
None,
) )
.expect("the witness resolves over committed state"); .expect("the witness resolves over committed state");
for ground in [ for ground in [
@ -14372,7 +14428,7 @@ mod clock_tests {
panic!("the operations helper builds an admitted patch"); panic!("the operations helper builds an admitted patch");
}; };
let resolved = let resolved =
patch::resolve_patch(&kernel.state, CommandId::new(), patch, None).expect("resolves"); patch::resolve_patch(&kernel.state, CommandId::new(), patch, None, None).expect("resolves");
assert!( assert!(
resolved.scale_intent.is_none(), resolved.scale_intent.is_none(),
"an Active patch resolved a scale intent" "an Active patch resolved a scale intent"
@ -14382,6 +14438,292 @@ mod clock_tests {
assert_eq!(kernel.state.scale_intent, authored); assert_eq!(kernel.state.scale_intent, authored);
} }
// ---- lens weights: world data, genesis, and the owner command --------
fn every_lens_at_zero() -> LensWeights {
LensWeights::new(Lens::ALL.into_iter().map(|lens| (lens, 0)).collect())
}
#[test]
fn genesis_writes_the_lens_weights_it_was_given() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("world.cc");
let given = LensWeights::new(BTreeMap::from([(Lens::Patina, 2), (Lens::Numen, 1)]));
let mut input = creation(CommandId::new(), "Flavored");
input.lens_weights = given.clone();
let (kernel, _) = WorldKernel::create(&path, input, &auth_principal(owner())).unwrap();
let created = kernel.snapshot().unwrap();
assert_eq!(created.lens_weights, given);
assert_eq!(kernel.state.lens_weights, given);
let world_id = created.world_id;
drop(kernel);
let replayed = WorldKernel::open(&path, world_id).unwrap();
assert_eq!(replayed.snapshot().unwrap(), created);
assert_eq!(replayed.state.state_digest, created.state_digest);
}
#[test]
fn a_genesis_lens_set_that_never_draws_is_refused() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("world.cc");
let mut input = creation(CommandId::new(), "Flavorless");
input.lens_weights = every_lens_at_zero();
let Err(error) = WorldKernel::create(&path, input, &auth_principal(owner())) else {
panic!("a genesis lens set that never draws was admitted");
};
let KernelError::PatchRejected(mismatches) = error else {
panic!("expected a rejected genesis patch, got {error:?}");
};
assert_eq!(mismatches, vec![Mismatch::LensWeightsNeverDraw]);
assert!(!path.exists(), "a refused genesis wrote a world file");
// The same creation with one real weight among the zeros is admitted.
let mut input = creation(CommandId::new(), "Flavorless");
let mut weights = BTreeMap::from_iter(every_lens_at_zero().iter());
weights.insert(Lens::Veil, 1);
input.lens_weights = LensWeights::new(weights);
WorldKernel::create(&path, input, &auth_principal(owner())).expect("one nonzero weight");
}
/// Soul falsification, the lens twin of the scale intent's write-once
/// test: an Active patch resolves no weights; only the owner's
/// `SetLensWeights` replaces them, in Draft and in Active; every other
/// caller is `Unauthorized`; a set that never draws is refused; and
/// `apply_effect` re-decides the owner against a hand-built effect.
#[test]
fn soul_the_lens_weights_are_replaced_only_by_the_owner_command() {
let directory = tempfile::tempdir().unwrap();
let (mut kernel, _) = WorldKernel::create(
directory.path().join("world.cc"),
creation(CommandId::new(), "Owner Flavor"),
&auth_principal(owner()),
)
.unwrap();
let draft_weights = LensWeights::new(BTreeMap::from([
(Lens::Patina, 0),
(Lens::Charter, 3),
(Lens::Ledger, 1),
]));
let draft = kernel.snapshot().unwrap();
submit_owner(
&mut kernel,
&draft,
CommandBody::SetLensWeights {
weights: draft_weights.clone(),
},
);
assert_eq!(kernel.state.revision, draft.revision + 1);
assert_eq!(kernel.state.lens_weights, draft_weights);
let active = activate(&mut kernel);
let active_weights = LensWeights::new(BTreeMap::from([
(Lens::Hearth, 5),
(Lens::Tangle, 0),
(Lens::Ember, 2),
]));
submit_owner(
&mut kernel,
&active,
CommandBody::SetLensWeights {
weights: active_weights.clone(),
},
);
assert_eq!(kernel.state.revision, active.revision + 1);
assert_eq!(kernel.state.lens_weights, active_weights);
// An Active patch is handed no weights, so it cannot resolve any.
let resolved = patch::resolve_patch(
&kernel.state,
CommandId::new(),
&operations_patch(&kernel),
None,
None,
)
.expect("resolves");
assert!(resolved.lens_weights.is_none(), "an Active patch resolved lens weights");
let after_patch = kernel.snapshot().unwrap();
let patch = operations_patch(&kernel);
submit_owner(
&mut kernel,
&after_patch,
CommandBody::AdmitPatch {
answers: None,
patch,
},
);
assert_eq!(kernel.state.lens_weights, active_weights);
// Every caller but the owner, with real weights.
let proposed = super::tests::stock_weights();
let before = kernel.state.clone();
for caller in [
CallerId::Principal(human_principal()),
CallerId::Principal(PrincipalId::new("stranger@example.test")),
elaborator(JurisdictionKey::Uncovered),
CallerId::System(SystemCapability::Clock),
CallerId::System(SystemCapability::Consumer {
consumer: ConsumerId::of_name("lens-consumer"),
}),
] {
let result = submit_as(
&mut kernel,
caller.clone(),
CommandBody::SetLensWeights {
weights: proposed.clone(),
},
);
assert!(
matches!(result, Err(KernelError::Unauthorized)),
"{caller:?} replaced the lens weights: {result:?}"
);
}
assert_eq!(kernel.state, before);
// The owner, with every lens named at zero.
let result = submit_as(
&mut kernel,
CallerId::Principal(owner()),
CommandBody::SetLensWeights {
weights: every_lens_at_zero(),
},
);
let Err(KernelError::PatchRejected(mismatches)) = result else {
panic!("a lens set that never draws was admitted: {result:?}");
};
assert_eq!(mismatches, vec![Mismatch::LensWeightsNeverDraw]);
assert_eq!(kernel.state, before);
// The owner, with valid weights, against the revision before the
// last commit.
let stale = command(
&draft,
CommandId::new(),
CallerId::Principal(owner()),
CommandBody::SetLensWeights {
weights: proposed.clone(),
},
);
assert!(matches!(
kernel.submit(stale, &auth_principal(owner())),
Err(KernelError::RevisionMismatch { .. })
));
assert_eq!(kernel.state, before);
// `apply_effect` re-decides: a hand-built effect under a non-owner or
// a system caller never applies, and neither does one that never draws.
for (caller, weights) in [
(CallerId::Principal(human_principal()), proposed.clone()),
(CallerId::System(SystemCapability::Clock), proposed.clone()),
(CallerId::Principal(owner()), every_lens_at_zero()),
] {
let mut candidate = kernel.state.clone();
let error = apply_effect(
&mut candidate,
CommandId::new(),
&caller,
&WorldEffect::LensWeightsReplaced { weights },
)
.unwrap_err();
assert!(
matches!(
error,
KernelError::Invariant(_) | KernelError::Unauthorized
),
"{caller:?}: {error:?}"
);
assert_eq!(candidate, kernel.state);
}
// The honest effect applies through the same arm.
let mut candidate = kernel.state.clone();
apply_effect(
&mut candidate,
CommandId::new(),
&CallerId::Principal(owner()),
&WorldEffect::LensWeightsReplaced {
weights: proposed.clone(),
},
)
.expect("the owner's effect applies");
assert_eq!(candidate.lens_weights, proposed);
// Identical weights from the owner are still a commit.
let current = kernel.snapshot().unwrap();
submit_owner(
&mut kernel,
&current,
CommandBody::SetLensWeights {
weights: active_weights.clone(),
},
);
assert_eq!(kernel.state.revision, current.revision + 1);
assert_eq!(kernel.state.lens_weights, active_weights);
}
/// An owner commit that adds a commitment to the first person: a real
/// Active patch with no answer, and nothing to do with lenses.
fn operations_patch(kernel: &WorldKernel) -> WorldPatch {
let subject = kernel
.state
.subjects
.iter()
.find(|(_, state)| state.kind == SubjectKind::Person)
.map(|(id, _)| *id)
.expect("a person");
let CommandBody::AdmitPatch { patch, .. } = operations(vec![ComponentOp::CreateCommitment {
subject: Ref::Existing(subject),
counterparty: None,
kind: CommitmentKind::Goal,
due: FictionalMinutes(kernel.state.now.0 + 500),
period: None,
checks: Vec::new(),
statement: Statement::new("A promise kept with no flavor at all.").unwrap(),
}]) else {
panic!("the operations helper builds an admitted patch");
};
patch
}
#[test]
fn lens_weights_enter_the_state_digest_and_no_scope_digest() {
let directory = tempfile::tempdir().unwrap();
let (mut kernel, _) = WorldKernel::create(
directory.path().join("world.cc"),
creation(CommandId::new(), "Digest Flavor"),
&auth_principal(owner()),
)
.unwrap();
let active = activate(&mut kernel);
let mut other = kernel.state.clone();
other.lens_weights =
LensWeights::new(BTreeMap::from([(Lens::Patina, 1), (Lens::Veil, 7)]));
assert_ne!(
state_digest(&other).unwrap(),
state_digest(&kernel.state).unwrap(),
"lens weights did not enter the state digest"
);
let digests = |kernel: &WorldKernel| -> BTreeMap<SubjectId, ScopeDigest> {
derive_opportunities(&kernel.state)
.unwrap()
.into_iter()
.map(|opportunity| (opportunity.scope.subject_id, opportunity.scope_digest))
.collect()
};
let before = digests(&kernel);
assert!(!before.is_empty(), "the active fixture derives opportunities");
submit_owner(
&mut kernel,
&active,
CommandBody::SetLensWeights {
weights: other.lens_weights.clone(),
},
);
assert_ne!(kernel.state.state_digest, active.state_digest);
assert_eq!(digests(&kernel), before, "lens weights entered a scope digest");
}
/// Soul falsification: a discharge names one subject's own live key. /// Soul falsification: a discharge names one subject's own live key.
#[test] #[test]
fn soul_discharging_a_foreign_or_already_discharged_commitment_is_refused() { fn soul_discharging_a_foreign_or_already_discharged_commitment_is_refused() {
@ -15029,6 +15371,7 @@ mod clock_tests {
command_id, command_id,
&shed_under(clockwork.dead_end, "shed"), &shed_under(clockwork.dead_end, "shed"),
None, None,
None,
) )
.expect("the patch resolves"); .expect("the patch resolves");
let effect = WorldEffect::PatchAdmitted { let effect = WorldEffect::PatchAdmitted {

View file

@ -188,6 +188,7 @@ impl WorldMailbox {
evidence: Vec::new(), evidence: Vec::new(),
}, },
scale_intent, scale_intent,
lens_weights: input.lens_weights,
}, },
AuthenticatedCaller::verified_principal(principal_id), AuthenticatedCaller::verified_principal(principal_id),
) )
@ -933,6 +934,7 @@ mod tests {
fn creation(id: CommandId, title: &str) -> CreateWorld { fn creation(id: CommandId, title: &str) -> CreateWorld {
CreateWorld { CreateWorld {
lens_weights: crate::tests::stock_weights(),
id, id,
owner: PrincipalId::new("owner"), owner: PrincipalId::new("owner"),
title: title.into(), title: title.into(),
@ -1354,6 +1356,7 @@ mod tests {
let created = mailbox let created = mailbox
.create_fixture( .create_fixture(
CreateWorld { CreateWorld {
lens_weights: crate::tests::stock_weights(),
id: CommandId::new(), id: CommandId::new(),
owner: owner.clone(), owner: owner.clone(),
title: "Soul Stamping".into(), title: "Soul Stamping".into(),

View file

@ -14,8 +14,8 @@
use super::clock::{FictionalMinutes, TickMinutes}; use super::clock::{FictionalMinutes, TickMinutes};
use super::tool_schema; use super::tool_schema;
use super::{ use super::{
AffordanceId, CommandId, ControllerAssignment, ControllerId, EdgeId, EntityId, NewController, AffordanceId, CommandId, ControllerAssignment, ControllerId, EdgeId, EntityId, LensWeights,
SubjectId, SubjectKind, SubjectState, WorldId, NewController, SubjectId, SubjectKind, SubjectState, WorldId,
}; };
use codex_connector::CodexToolDefinition; use codex_connector::CodexToolDefinition;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -1552,6 +1552,9 @@ pub enum Mismatch {
/// The permille weights sum over 1000: weights distribute the target and /// The permille weights sum over 1000: weights distribute the target and
/// never raise it. /// never raise it.
ScaleWeightsExceedWhole, ScaleWeightsExceedWhole,
/// A lens weight set with no nonzero weight, at genesis or in
/// `SetLensWeights`. A single zero weight is admitted and never draws.
LensWeightsNeverDraw,
/// A jurisdictional author wrote outside its jurisdiction. The owner is /// A jurisdictional author wrote outside its jurisdiction. The owner is
/// unconfined and never sees this. /// unconfined and never sees this.
OutsideJurisdiction { OutsideJurisdiction {
@ -1901,6 +1904,9 @@ pub(super) struct ResolvedPatch {
/// carries no intent, so the write-once rule is the command shape rather /// carries no intent, so the write-once rule is the command shape rather
/// than a check. /// than a check.
pub(super) scale_intent: Option<WorldScaleIntent>, pub(super) scale_intent: Option<WorldScaleIntent>,
/// `Some` on the genesis lane and nowhere else, as `scale_intent` is. After
/// genesis the weights change only through `SetLensWeights`.
pub(super) lens_weights: Option<LensWeights>,
} }
impl ResolvedPatch { impl ResolvedPatch {
@ -2943,6 +2949,7 @@ pub(super) fn resolve_patch(
command_id: CommandId, command_id: CommandId,
patch: &WorldPatch, patch: &WorldPatch,
scale_intent: Option<&WorldScaleIntentRef>, scale_intent: Option<&WorldScaleIntentRef>,
lens_weights: Option<&LensWeights>,
) -> Result<ResolvedPatch, Vec<Mismatch>> { ) -> Result<ResolvedPatch, Vec<Mismatch>> {
let world_id = state.world_id; let world_id = state.world_id;
let admits_human = super::admits_human(state.revision, &state.subjects); let admits_human = super::admits_human(state.revision, &state.subjects);
@ -4614,6 +4621,12 @@ pub(super) fn resolve_patch(
} }
} }
if let Some(weights) = lens_weights
&& !weights.draws()
{
mismatches.push(Mismatch::LensWeightsNeverDraw);
}
if patch.declarations.is_empty() && patch.operations.is_empty() { if patch.declarations.is_empty() && patch.operations.is_empty() {
mismatches.push(Mismatch::NoCanonicalChange); mismatches.push(Mismatch::NoCanonicalChange);
} }
@ -5283,6 +5296,7 @@ pub(super) fn resolve_patch(
}) })
.collect(), .collect(),
}), }),
lens_weights: lens_weights.cloned(),
}) })
} }
@ -8441,8 +8455,8 @@ mod tests {
assert_ne!(admitted, genesis_human); assert_ne!(admitted, genesis_human);
let command_id = CommandId::new(); let command_id = CommandId::new();
let first = resolve_patch(&kernel.state, command_id, &patch, None).unwrap(); let first = resolve_patch(&kernel.state, command_id, &patch, None, None).unwrap();
let second = resolve_patch(&kernel.state, command_id, &patch, None).unwrap(); let second = resolve_patch(&kernel.state, command_id, &patch, None, None).unwrap();
assert_eq!(first, second); assert_eq!(first, second);
} }
@ -8585,7 +8599,7 @@ mod tests {
Some(Ref::Draft(DraftHandle::new("rhythm-road"))), Some(Ref::Draft(DraftHandle::new("rhythm-road"))),
), ),
]); ]);
let resolved = resolve_patch(&kernel.state, CommandId::new(), &patch, None).unwrap(); let resolved = resolve_patch(&kernel.state, CommandId::new(), &patch, None, None).unwrap();
let honest = super::super::WorldEffect::PatchAdmitted { let honest = super::super::WorldEffect::PatchAdmitted {
resolved: resolved.clone(), resolved: resolved.clone(),
answers: None, answers: None,
@ -8788,6 +8802,7 @@ mod tests {
for edge_id in [topology.span, topology.shutter, topology.toll] { for edge_id in [topology.span, topology.shutter, topology.toll] {
let forged = super::super::WorldEffect::PatchAdmitted { let forged = super::super::WorldEffect::PatchAdmitted {
resolved: ResolvedPatch { resolved: ResolvedPatch {
lens_weights: None,
subjects: Vec::new(), subjects: Vec::new(),
entities: Vec::new(), entities: Vec::new(),
routes: Vec::new(), routes: Vec::new(),

View file

@ -19,8 +19,9 @@
use chrono::{Duration, Utc}; use chrono::{Duration, Utc};
use ghostlight::{ use ghostlight::{
CommandBody, CommandId, ControllerMode, ControllerPort, CreateWorldIntent, DecisionInvocation, CommandBody, CommandId, ControllerMode, ControllerPort, CreateWorldIntent, DecisionInvocation,
DecisionOpportunity, KernelError, MailboxError, PrincipalCommandIntent, Statement, DecisionOpportunity, KernelError, Lens, LensWeights, MailboxError, PrincipalCommandIntent,
SubjectId, SubmitReceipt, VerifiedPrincipalEvidence, WorldMailbox, WorldPhase, WorldSnapshot, Statement, SubjectId, SubmitReceipt, VerifiedPrincipalEvidence, WorldMailbox, WorldPhase,
WorldSnapshot,
}; };
use serde_json::{Value, json}; use serde_json::{Value, json};
use std::collections::BTreeMap; use std::collections::BTreeMap;
@ -33,6 +34,7 @@ struct World {
_directory: tempfile::TempDir, _directory: tempfile::TempDir,
_owner: tokio::task::JoinHandle<()>, _owner: tokio::task::JoinHandle<()>,
mailbox: WorldMailbox, mailbox: WorldMailbox,
principal: VerifiedPrincipalEvidence,
} }
impl World { impl World {
@ -52,6 +54,9 @@ impl World {
operational_agent_label: Some("Operational Agent".into()), operational_agent_label: Some("Operational Agent".into()),
targets: BTreeMap::new(), targets: BTreeMap::new(),
jurisdictions: Vec::new(), jurisdictions: Vec::new(),
// An external crate has no library fixture; it states its
// own weights, as every consumer must.
lens_weights: LensWeights::new(BTreeMap::from([(Lens::Patina, 1)])),
}, },
&principal, &principal,
) )
@ -76,6 +81,7 @@ impl World {
_directory: directory, _directory: directory,
_owner: owner, _owner: owner,
mailbox, mailbox,
principal,
}; };
assert_eq!(world.snapshot().await.phase, WorldPhase::Active); assert_eq!(world.snapshot().await.phase, WorldPhase::Active);
world world
@ -477,3 +483,91 @@ async fn a_stale_issued_scope_digest_is_refused_and_commits_nothing() {
assert_eq!(world.committed().await, before, "{what}"); assert_eq!(world.committed().await, before, "{what}");
} }
} }
/// Every stock lens named, with real weights: the shape a consumer's weight
/// control produces, so a refusal below is about the caller and not the value.
fn real_weights() -> LensWeights {
LensWeights::new(BTreeMap::from([
(Lens::Patina, 3),
(Lens::Charter, 1),
(Lens::Ledger, 0),
(Lens::Hearth, 2),
(Lens::Tangle, 1),
(Lens::Veil, 1),
(Lens::Ember, 1),
(Lens::Numen, 1),
]))
}
/// A second verified principal holds a live session and valid weights and is
/// not the owner. The kernel refuses it and nothing commits; the same body
/// from the owner then commits, so the refusal is not an inert harness.
#[tokio::test]
async fn set_lens_weights_from_a_non_owner_is_refused_and_commits_nothing() {
let world = World::active().await;
let snapshot = world.snapshot().await;
let stranger =
VerifiedPrincipalEvidence::new("external-admission-stranger", Utc::now() + Duration::hours(1));
let before = world.committed().await;
let result = world
.mailbox
.submit_principal(
PrincipalCommandIntent {
id: CommandId::new(),
world_id: snapshot.world_id,
expected_revision: snapshot.revision,
body: CommandBody::SetLensWeights {
weights: real_weights(),
},
},
&stranger,
)
.await;
assert!(
matches!(
result,
Err(MailboxError::Kernel(KernelError::Unauthorized))
),
"a non-owner replaced the lens weights: {result:?}"
);
assert_eq!(world.committed().await, before);
assert_ne!(world.snapshot().await.lens_weights, real_weights());
let receipt = world
.mailbox
.submit_principal(
PrincipalCommandIntent {
id: CommandId::new(),
world_id: snapshot.world_id,
expected_revision: snapshot.revision,
body: CommandBody::SetLensWeights {
weights: real_weights(),
},
},
&world.principal,
)
.await
.expect("the owner replaces the lens weights");
assert!(matches!(receipt, SubmitReceipt::Applied(_)));
let after = world.snapshot().await;
assert_eq!(after.revision, before.0 + 1);
assert_eq!(after.lens_weights, real_weights());
}
/// A lens name the library does not know cannot be decoded into a command
/// body, beside names it does. This is a serde limit, stated as one: the body
/// never exists, so no kernel admission is being proven here.
#[test]
fn set_lens_weights_with_an_unknown_lens_does_not_decode() {
let refused = serde_json::from_value::<CommandBody>(json!({
"type": "set_lens_weights",
"weights": {"patina": 1, "veil ": 1},
}));
assert!(refused.is_err(), "an unknown lens decoded: {refused:?}");
let accepted = serde_json::from_value::<CommandBody>(json!({
"type": "set_lens_weights",
"weights": {"patina": 1, "veil": 1},
}))
.expect("known lens names decode");
assert!(matches!(accepted, CommandBody::SetLensWeights { .. }));
}