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:
parent
3cb6a74bb0
commit
d06fd7ec2e
10 changed files with 815 additions and 33 deletions
|
|
@ -44,9 +44,9 @@ pub(crate) struct EveRouteHint {
|
|||
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.
|
||||
const CREATE_BINDINGS: [&str; 7] = [
|
||||
const CREATE_BINDINGS: [&str; 8] = [
|
||||
"title",
|
||||
"brief",
|
||||
"subject_label",
|
||||
|
|
@ -54,6 +54,7 @@ const CREATE_BINDINGS: [&str; 7] = [
|
|||
"operational_agent_label",
|
||||
"targets",
|
||||
"jurisdictions",
|
||||
"lens_weights",
|
||||
];
|
||||
|
||||
const SEED_BINDINGS: [&str; 2] = ["vault_scope", "brief"];
|
||||
|
|
@ -256,6 +257,11 @@ pub(crate) fn authenticated_surface(
|
|||
|
||||
match snapshot {
|
||||
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([
|
||||
json!({
|
||||
"id":"world.create.title",
|
||||
|
|
@ -299,6 +305,13 @@ pub(crate) fn authenticated_surface(
|
|||
"stateBindings":[local_draft("jurisdictions", "json")],
|
||||
"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(
|
||||
"world.create",
|
||||
"Create world",
|
||||
|
|
@ -309,7 +322,7 @@ pub(crate) fn authenticated_surface(
|
|||
]);
|
||||
commands.push(command_descriptor(
|
||||
"world.create",
|
||||
"ghostlight.world_create.v3",
|
||||
"ghostlight.world_create.v4",
|
||||
&CREATE_BINDINGS,
|
||||
"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.complete" => "heimdall.auth_complete_command.v1",
|
||||
"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.activate" => "ghostlight.world_activate.v0",
|
||||
"world.advance_time" => "ghostlight.world_advance_time.v0",
|
||||
|
|
@ -758,6 +771,27 @@ mod tests {
|
|||
assert!(!encoded.contains("session_zero"));
|
||||
assert!(!encoded.contains("campaign"));
|
||||
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]
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ use ghostlight::{
|
|||
ConsumerPort, ConsumerRegistry, ControllerError, ControllerModels, ControllerPendingReason,
|
||||
ControllerRunner, ControllerWorkCustody, Cover, CoverBudget, CreateJurisdictionIntent,
|
||||
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,
|
||||
SubmitReceipt, TickMinutes, VaultEvidenceSource, VerifiedPrincipalEvidence, WorldMailbox,
|
||||
WorldPhase, WorldSnapshot, derive_cover, open_controller_work, open_inference,
|
||||
|
|
@ -159,7 +159,7 @@ struct CreatePayload {
|
|||
title: String,
|
||||
/// 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,
|
||||
/// which is what `world_create.v3` means.
|
||||
/// which is what `world_create.v4` means.
|
||||
brief: String,
|
||||
subject_label: String,
|
||||
#[serde(default)]
|
||||
|
|
@ -169,13 +169,17 @@ struct CreatePayload {
|
|||
/// 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
|
||||
/// 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>,
|
||||
/// The jurisdiction roots, declared by genesis beside the commons because
|
||||
/// `resolve_patch` only resolves roots the same patch declares. A duplicate
|
||||
/// handle and a permille sum over 1000 are refused by the resolver, not
|
||||
/// pre-checked here: a pre-check would be a second reducer.
|
||||
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)]
|
||||
|
|
@ -1471,6 +1475,7 @@ async fn execute_world(
|
|||
permille: root.permille,
|
||||
})
|
||||
.collect(),
|
||||
lens_weights: payload.lens_weights,
|
||||
},
|
||||
verified_principal,
|
||||
)
|
||||
|
|
@ -1860,6 +1865,12 @@ fn configured_cover_budget() -> anyhow::Result<CoverBudget> {
|
|||
.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`
|
||||
/// refusals are already retryable faults, so a pool above that quota degrades
|
||||
/// to retry rather than to corruption.
|
||||
|
|
@ -2883,14 +2894,15 @@ mod tests {
|
|||
&fixture.cookie,
|
||||
invocation(
|
||||
"world.create",
|
||||
"ghostlight.world_create.v3",
|
||||
"ghostlight.world_create.v4",
|
||||
0,
|
||||
json!({
|
||||
"title":"Cutover World",
|
||||
"brief":"",
|
||||
"subject_label":"Operator",
|
||||
"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(),
|
||||
),
|
||||
|
|
@ -2964,14 +2976,15 @@ mod tests {
|
|||
let id = uuid::Uuid::new_v4().to_string();
|
||||
let command = invocation(
|
||||
"world.create",
|
||||
"ghostlight.world_create.v3",
|
||||
"ghostlight.world_create.v4",
|
||||
0,
|
||||
json!({
|
||||
"title":"Retry World",
|
||||
"brief":"",
|
||||
"subject_label":"Operator",
|
||||
"targets":{},
|
||||
"jurisdictions":[]
|
||||
"jurisdictions":[],
|
||||
"lens_weights":{"patina":1,"charter":1,"ledger":1,"hearth":1,"tangle":1,"veil":1,"ember":1,"numen":1}
|
||||
}),
|
||||
&id,
|
||||
);
|
||||
|
|
@ -3147,6 +3160,7 @@ mod tests {
|
|||
operational_agent_label: Some("Operational Agent".into()),
|
||||
targets,
|
||||
jurisdictions,
|
||||
lens_weights: uniform_lens_weights(),
|
||||
},
|
||||
&principal,
|
||||
)
|
||||
|
|
@ -4003,10 +4017,10 @@ mod tests {
|
|||
|
||||
// ---- The seed command ------------------------------------------------
|
||||
|
||||
/// Spec test 1. `world_create.v2` is not kept alive beside v3: an
|
||||
/// invocation announcing it dies at validation before any handler runs, and
|
||||
/// a payload that announces v3 but omits the brief or the scale target is a
|
||||
/// payload error rather than a defaulted intent. Neither creates a world.
|
||||
/// Spec test 1. The previous create schema is not kept alive beside
|
||||
/// `world_create.v4`: a complete payload announcing it dies at validation before any handler runs,
|
||||
/// and a payload that announces v4 but omits the lens weights or the brief
|
||||
/// is a payload error rather than a defaulted intent. None creates a world.
|
||||
#[tokio::test]
|
||||
async fn a_stale_create_payload_is_refused() {
|
||||
let fixture = fixture().await;
|
||||
|
|
@ -4015,9 +4029,9 @@ mod tests {
|
|||
&fixture.cookie,
|
||||
invocation(
|
||||
"world.create",
|
||||
"ghostlight.world_create.v2",
|
||||
"ghostlight.world_create.v3",
|
||||
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(),
|
||||
),
|
||||
)
|
||||
|
|
@ -4025,14 +4039,39 @@ mod tests {
|
|||
assert_eq!(stale["state"], "denied");
|
||||
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(
|
||||
&fixture.state,
|
||||
&fixture.cookie,
|
||||
invocation(
|
||||
"world.create",
|
||||
"ghostlight.world_create.v3",
|
||||
"ghostlight.world_create.v4",
|
||||
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(),
|
||||
),
|
||||
)
|
||||
|
|
@ -4040,10 +4079,62 @@ mod tests {
|
|||
assert_eq!(partial["state"], "denied");
|
||||
assert!(
|
||||
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.
|
||||
fn surface_buttons(node: &Value, into: &mut Vec<String>) {
|
||||
if node["kind"] == "control.button"
|
||||
|
|
|
|||
|
|
@ -172,6 +172,7 @@ pub(super) fn exercise(
|
|||
evidence: Vec::new(),
|
||||
},
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.map_err(KernelError::PatchRejected)?
|
||||
.operations
|
||||
|
|
|
|||
|
|
@ -1219,7 +1219,7 @@ mod tests {
|
|||
}],
|
||||
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");
|
||||
let effect = super::super::WorldEffect::PatchAdmitted {
|
||||
answers: None,
|
||||
|
|
@ -2234,7 +2234,7 @@ mod tests {
|
|||
assert!(super::super::journal::verify_state_shape(&previous).is_err());
|
||||
assert_eq!(
|
||||
super::super::STATE_SCHEMA,
|
||||
"ghostlight.world_state.consumer.v4"
|
||||
"ghostlight.world_state.consumer.v5"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6667,6 +6667,7 @@ mod tests {
|
|||
material: None,
|
||||
};
|
||||
let snapshot = WorldSnapshot {
|
||||
lens_weights: crate::tests::stock_weights(),
|
||||
world_id: opportunity.world_id,
|
||||
revision: opportunity.revision,
|
||||
phase: WorldPhase::Active,
|
||||
|
|
@ -6924,6 +6925,7 @@ mod tests {
|
|||
open: true,
|
||||
};
|
||||
let snapshot = WorldSnapshot {
|
||||
lens_weights: crate::tests::stock_weights(),
|
||||
world_id: opportunity.world_id,
|
||||
revision: opportunity.revision,
|
||||
phase: WorldPhase::Active,
|
||||
|
|
@ -7419,6 +7421,7 @@ mod tests {
|
|||
affordance_ids: vec![speak_affordance],
|
||||
};
|
||||
let snapshot = WorldSnapshot {
|
||||
lens_weights: crate::tests::stock_weights(),
|
||||
world_id: opportunity.world_id,
|
||||
revision: opportunity.revision,
|
||||
phase: WorldPhase::Active,
|
||||
|
|
@ -8415,6 +8418,7 @@ mod tests {
|
|||
let creation = mailbox
|
||||
.create_fixture(
|
||||
CreateWorld {
|
||||
lens_weights: crate::tests::stock_weights(),
|
||||
id: CommandId::new(),
|
||||
owner: owner.clone(),
|
||||
title: "Controller Fixture".into(),
|
||||
|
|
@ -9010,6 +9014,7 @@ mod tests {
|
|||
let creation = mailbox
|
||||
.create_fixture(
|
||||
CreateWorld {
|
||||
lens_weights: crate::tests::stock_weights(),
|
||||
id: CommandId::new(),
|
||||
owner: owner.clone(),
|
||||
title: "Midnight Roll Call".into(),
|
||||
|
|
@ -9413,6 +9418,7 @@ mod tests {
|
|||
let creation = mailbox
|
||||
.create_fixture(
|
||||
CreateWorld {
|
||||
lens_weights: crate::tests::stock_weights(),
|
||||
id: CommandId::new(),
|
||||
owner: owner.clone(),
|
||||
title: "Elaboration Fixture".into(),
|
||||
|
|
@ -9703,6 +9709,7 @@ mod tests {
|
|||
let creation = mailbox
|
||||
.create_fixture(
|
||||
CreateWorld {
|
||||
lens_weights: crate::tests::stock_weights(),
|
||||
id: CommandId::new(),
|
||||
owner: owner.clone(),
|
||||
title: "Cover Fixture".into(),
|
||||
|
|
@ -10250,6 +10257,7 @@ mod tests {
|
|||
let creation = mailbox
|
||||
.create_fixture(
|
||||
CreateWorld {
|
||||
lens_weights: crate::tests::stock_weights(),
|
||||
id: CommandId::new(),
|
||||
owner: owner.clone(),
|
||||
title: "Apart Fixture".into(),
|
||||
|
|
@ -10695,6 +10703,7 @@ mod tests {
|
|||
mailbox
|
||||
.create_fixture(
|
||||
CreateWorld {
|
||||
lens_weights: crate::tests::stock_weights(),
|
||||
id: CommandId::new(),
|
||||
owner: owner.clone(),
|
||||
title: "Seed Fixture".into(),
|
||||
|
|
@ -11447,6 +11456,7 @@ mod tests {
|
|||
mailbox
|
||||
.create(
|
||||
CreateWorldIntent {
|
||||
lens_weights: crate::tests::stock_weights(),
|
||||
id: CommandId::new(),
|
||||
title: "The Whole Road".into(),
|
||||
brief: String::new(),
|
||||
|
|
@ -11607,6 +11617,7 @@ mod tests {
|
|||
let owner = PrincipalId::new("seed-owner");
|
||||
let authenticated = AuthenticatedCaller::fixture(CallerId::Principal(owner.clone()));
|
||||
let genesis = |intent: WorldScaleIntentRef, roots: &[&str]| CreateWorld {
|
||||
lens_weights: crate::tests::stock_weights(),
|
||||
id: CommandId::new(),
|
||||
owner: owner.clone(),
|
||||
title: "Two Roots".into(),
|
||||
|
|
@ -13135,6 +13146,7 @@ mod tests {
|
|||
let creation = mailbox
|
||||
.create_fixture(
|
||||
CreateWorld {
|
||||
lens_weights: crate::tests::stock_weights(),
|
||||
id: CommandId::new(),
|
||||
owner: owner.clone(),
|
||||
title: "Witness Fixture".into(),
|
||||
|
|
@ -13634,6 +13646,7 @@ mod tests {
|
|||
let creation = mailbox
|
||||
.create_fixture(
|
||||
CreateWorld {
|
||||
lens_weights: crate::tests::stock_weights(),
|
||||
id: CommandId::new(),
|
||||
owner: owner.clone(),
|
||||
title: "Interruption Fixture".into(),
|
||||
|
|
|
|||
|
|
@ -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 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
|
||||
// occupied: a jurisdiction names live ground under a canonical kind, an
|
||||
// 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 authenticated = AuthenticatedCaller::fixture(CallerId::Principal(owner.clone()));
|
||||
let creation = CreateWorld {
|
||||
lens_weights: crate::tests::stock_weights(),
|
||||
id: CommandId::new(),
|
||||
owner: owner.clone(),
|
||||
title: "Before".into(),
|
||||
|
|
@ -1144,6 +1150,7 @@ mod tests {
|
|||
let owner = PrincipalId::new("owner@example.test");
|
||||
let authenticated = AuthenticatedCaller::fixture(CallerId::Principal(owner.clone()));
|
||||
let creation = CreateWorld {
|
||||
lens_weights: crate::tests::stock_weights(),
|
||||
id: CommandId::new(),
|
||||
owner: owner.clone(),
|
||||
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,
|
||||
/// so a committed motion rewritten after the fact no longer reduces from
|
||||
/// 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 creation_id = CommandId::new();
|
||||
let creation = CreateWorld {
|
||||
lens_weights: crate::tests::stock_weights(),
|
||||
id: creation_id,
|
||||
owner: owner.clone(),
|
||||
title: "Admitted".into(),
|
||||
|
|
|
|||
|
|
@ -171,8 +171,8 @@ impl VerifiedPrincipalEvidence {
|
|||
}
|
||||
}
|
||||
|
||||
pub const STATE_SCHEMA: &str = "ghostlight.world_state.consumer.v4";
|
||||
pub(crate) const COMMIT_SCHEMA: &str = "ghostlight.world_commit.consumer.v4";
|
||||
pub const STATE_SCHEMA: &str = "ghostlight.world_state.consumer.v5";
|
||||
pub(crate) const COMMIT_SCHEMA: &str = "ghostlight.world_commit.consumer.v5";
|
||||
|
||||
/// Compatibility tag derived from [`STATE_SCHEMA`]: the trailing
|
||||
/// `<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
|
||||
/// no intent, so write-once is the command shape rather than a check.
|
||||
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
|
||||
|
|
@ -431,6 +434,9 @@ pub struct CreateWorldIntent {
|
|||
/// The jurisdiction roots genesis declares beside the commons, because
|
||||
/// `resolve_patch` only resolves roots the same patch declares.
|
||||
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
|
||||
|
|
@ -672,6 +678,14 @@ pub enum CommandBody {
|
|||
AdvanceTime {
|
||||
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)]
|
||||
|
|
@ -845,6 +859,10 @@ struct WorldState {
|
|||
now: FictionalMinutes,
|
||||
/// The authored scale target, written once by genesis and never mutated.
|
||||
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>,
|
||||
/// What an affordance *is*. World-authored, Draft-only, written by
|
||||
/// `admit_resolved` alone.
|
||||
|
|
@ -896,6 +914,11 @@ enum WorldEffect {
|
|||
to: FictionalMinutes,
|
||||
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
|
||||
|
|
@ -1113,6 +1136,7 @@ pub struct WorldSnapshot {
|
|||
/// commitment gazetteer.
|
||||
pub boundaries: Vec<CausalBoundary>,
|
||||
pub scale_deficit: Vec<ScaleDeficitRow>,
|
||||
pub lens_weights: LensWeights,
|
||||
pub state_digest: String,
|
||||
pub last_commit_digest: Option<String>,
|
||||
}
|
||||
|
|
@ -1318,6 +1342,7 @@ fn prepare_creation(
|
|||
input.id,
|
||||
&input.patch,
|
||||
Some(&input.scale_intent),
|
||||
Some(&input.lens_weights),
|
||||
)
|
||||
.map_err(KernelError::PatchRejected)?;
|
||||
Ok(PreparedCreation {
|
||||
|
|
@ -1640,7 +1665,7 @@ fn reduce(state: &WorldState, command: &CommandEnvelope) -> Result<WorldEffect,
|
|||
!(patch.declarations.is_empty() && patch.evidence.is_empty()),
|
||||
)?;
|
||||
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)?;
|
||||
if let Some(ground) = confinement {
|
||||
confine_to_ground(state, &resolved, ground).map_err(KernelError::PatchRejected)?;
|
||||
|
|
@ -1650,6 +1675,17 @@ fn reduce(state: &WorldState, command: &CommandEnvelope) -> Result<WorldEffect,
|
|||
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(),
|
||||
now: FictionalMinutes::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(),
|
||||
affordance_catalog: BTreeMap::new(),
|
||||
affordance_grants: BTreeMap::new(),
|
||||
|
|
@ -1812,6 +1851,7 @@ impl WorldState {
|
|||
command.id,
|
||||
&command.patch,
|
||||
Some(&command.scale_intent),
|
||||
Some(&command.lens_weights),
|
||||
)
|
||||
.map_err(KernelError::PatchRejected)?;
|
||||
if &expected != resolved {
|
||||
|
|
@ -2047,6 +2087,9 @@ fn admit_resolved(state: &mut WorldState, resolved: &ResolvedPatch) -> Result<()
|
|||
if let Some(intent) = &resolved.scale_intent {
|
||||
state.scale_intent = intent.clone();
|
||||
}
|
||||
if let Some(weights) = &resolved.lens_weights {
|
||||
state.lens_weights = weights.clone();
|
||||
}
|
||||
apply_operations(state, &resolved.operations, &resolved.evidence)
|
||||
}
|
||||
|
||||
|
|
@ -3718,6 +3761,7 @@ fn snapshot(state: &WorldState) -> Result<WorldSnapshot, KernelError> {
|
|||
now: state.now,
|
||||
boundaries: derive_boundaries(state)?,
|
||||
scale_deficit: derive_scale_deficit(state)?,
|
||||
lens_weights: state.lens_weights.clone(),
|
||||
state_digest: state.state_digest.clone(),
|
||||
last_commit_digest: state.last_commit_digest.clone(),
|
||||
})
|
||||
|
|
@ -4892,6 +4936,14 @@ fn apply_effect(
|
|||
.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(())
|
||||
}
|
||||
|
|
@ -5064,6 +5116,7 @@ mod tests {
|
|||
],
|
||||
},
|
||||
scale_intent: WorldScaleIntentRef::default(),
|
||||
lens_weights: stock_weights(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -8246,6 +8299,7 @@ mod custody_tests {
|
|||
let mut candidate = kernel.state.clone();
|
||||
let forged = WorldEffect::PatchAdmitted {
|
||||
resolved: ResolvedPatch {
|
||||
lens_weights: None,
|
||||
subjects: Vec::new(),
|
||||
entities: Vec::new(),
|
||||
routes: Vec::new(),
|
||||
|
|
@ -8840,6 +8894,7 @@ mod custody_tests {
|
|||
|
||||
let forge = |operations: Vec<ResolvedOp>| WorldEffect::PatchAdmitted {
|
||||
resolved: ResolvedPatch {
|
||||
lens_weights: None,
|
||||
subjects: Vec::new(),
|
||||
entities: Vec::new(),
|
||||
routes: Vec::new(),
|
||||
|
|
@ -11493,6 +11548,7 @@ mod witness_tests {
|
|||
evidence: Vec::new(),
|
||||
},
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.expect("the witness resolves over committed state");
|
||||
for ground in [
|
||||
|
|
@ -14372,7 +14428,7 @@ mod clock_tests {
|
|||
panic!("the operations helper builds an admitted patch");
|
||||
};
|
||||
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!(
|
||||
resolved.scale_intent.is_none(),
|
||||
"an Active patch resolved a scale intent"
|
||||
|
|
@ -14382,6 +14438,292 @@ mod clock_tests {
|
|||
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,
|
||||
¤t,
|
||||
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.
|
||||
#[test]
|
||||
fn soul_discharging_a_foreign_or_already_discharged_commitment_is_refused() {
|
||||
|
|
@ -15029,6 +15371,7 @@ mod clock_tests {
|
|||
command_id,
|
||||
&shed_under(clockwork.dead_end, "shed"),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.expect("the patch resolves");
|
||||
let effect = WorldEffect::PatchAdmitted {
|
||||
|
|
|
|||
|
|
@ -188,6 +188,7 @@ impl WorldMailbox {
|
|||
evidence: Vec::new(),
|
||||
},
|
||||
scale_intent,
|
||||
lens_weights: input.lens_weights,
|
||||
},
|
||||
AuthenticatedCaller::verified_principal(principal_id),
|
||||
)
|
||||
|
|
@ -933,6 +934,7 @@ mod tests {
|
|||
|
||||
fn creation(id: CommandId, title: &str) -> CreateWorld {
|
||||
CreateWorld {
|
||||
lens_weights: crate::tests::stock_weights(),
|
||||
id,
|
||||
owner: PrincipalId::new("owner"),
|
||||
title: title.into(),
|
||||
|
|
@ -1354,6 +1356,7 @@ mod tests {
|
|||
let created = mailbox
|
||||
.create_fixture(
|
||||
CreateWorld {
|
||||
lens_weights: crate::tests::stock_weights(),
|
||||
id: CommandId::new(),
|
||||
owner: owner.clone(),
|
||||
title: "Soul Stamping".into(),
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@
|
|||
use super::clock::{FictionalMinutes, TickMinutes};
|
||||
use super::tool_schema;
|
||||
use super::{
|
||||
AffordanceId, CommandId, ControllerAssignment, ControllerId, EdgeId, EntityId, NewController,
|
||||
SubjectId, SubjectKind, SubjectState, WorldId,
|
||||
AffordanceId, CommandId, ControllerAssignment, ControllerId, EdgeId, EntityId, LensWeights,
|
||||
NewController, SubjectId, SubjectKind, SubjectState, WorldId,
|
||||
};
|
||||
use codex_connector::CodexToolDefinition;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
|
@ -1552,6 +1552,9 @@ pub enum Mismatch {
|
|||
/// The permille weights sum over 1000: weights distribute the target and
|
||||
/// never raise it.
|
||||
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
|
||||
/// unconfined and never sees this.
|
||||
OutsideJurisdiction {
|
||||
|
|
@ -1901,6 +1904,9 @@ pub(super) struct ResolvedPatch {
|
|||
/// carries no intent, so the write-once rule is the command shape rather
|
||||
/// than a check.
|
||||
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 {
|
||||
|
|
@ -2943,6 +2949,7 @@ pub(super) fn resolve_patch(
|
|||
command_id: CommandId,
|
||||
patch: &WorldPatch,
|
||||
scale_intent: Option<&WorldScaleIntentRef>,
|
||||
lens_weights: Option<&LensWeights>,
|
||||
) -> Result<ResolvedPatch, Vec<Mismatch>> {
|
||||
let world_id = state.world_id;
|
||||
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() {
|
||||
mismatches.push(Mismatch::NoCanonicalChange);
|
||||
}
|
||||
|
|
@ -5283,6 +5296,7 @@ pub(super) fn resolve_patch(
|
|||
})
|
||||
.collect(),
|
||||
}),
|
||||
lens_weights: lens_weights.cloned(),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -8441,8 +8455,8 @@ mod tests {
|
|||
assert_ne!(admitted, genesis_human);
|
||||
|
||||
let command_id = CommandId::new();
|
||||
let first = resolve_patch(&kernel.state, command_id, &patch, None).unwrap();
|
||||
let second = 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, None).unwrap();
|
||||
assert_eq!(first, second);
|
||||
}
|
||||
|
||||
|
|
@ -8585,7 +8599,7 @@ mod tests {
|
|||
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 {
|
||||
resolved: resolved.clone(),
|
||||
answers: None,
|
||||
|
|
@ -8788,6 +8802,7 @@ mod tests {
|
|||
for edge_id in [topology.span, topology.shutter, topology.toll] {
|
||||
let forged = super::super::WorldEffect::PatchAdmitted {
|
||||
resolved: ResolvedPatch {
|
||||
lens_weights: None,
|
||||
subjects: Vec::new(),
|
||||
entities: Vec::new(),
|
||||
routes: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -19,8 +19,9 @@
|
|||
use chrono::{Duration, Utc};
|
||||
use ghostlight::{
|
||||
CommandBody, CommandId, ControllerMode, ControllerPort, CreateWorldIntent, DecisionInvocation,
|
||||
DecisionOpportunity, KernelError, MailboxError, PrincipalCommandIntent, Statement,
|
||||
SubjectId, SubmitReceipt, VerifiedPrincipalEvidence, WorldMailbox, WorldPhase, WorldSnapshot,
|
||||
DecisionOpportunity, KernelError, Lens, LensWeights, MailboxError, PrincipalCommandIntent,
|
||||
Statement, SubjectId, SubmitReceipt, VerifiedPrincipalEvidence, WorldMailbox, WorldPhase,
|
||||
WorldSnapshot,
|
||||
};
|
||||
use serde_json::{Value, json};
|
||||
use std::collections::BTreeMap;
|
||||
|
|
@ -33,6 +34,7 @@ struct World {
|
|||
_directory: tempfile::TempDir,
|
||||
_owner: tokio::task::JoinHandle<()>,
|
||||
mailbox: WorldMailbox,
|
||||
principal: VerifiedPrincipalEvidence,
|
||||
}
|
||||
|
||||
impl World {
|
||||
|
|
@ -52,6 +54,9 @@ impl World {
|
|||
operational_agent_label: Some("Operational Agent".into()),
|
||||
targets: BTreeMap::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,
|
||||
)
|
||||
|
|
@ -76,6 +81,7 @@ impl World {
|
|||
_directory: directory,
|
||||
_owner: owner,
|
||||
mailbox,
|
||||
principal,
|
||||
};
|
||||
assert_eq!(world.snapshot().await.phase, WorldPhase::Active);
|
||||
world
|
||||
|
|
@ -477,3 +483,91 @@ async fn a_stale_issued_scope_digest_is_refused_and_commits_nothing() {
|
|||
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 { .. }));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue