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>,
|
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]
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
¤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.
|
/// 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 {
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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 { .. }));
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue