diff --git a/crates/ghostlight/src/consumer.rs b/crates/ghostlight/src/consumer.rs index bd78109..5740baf 100644 --- a/crates/ghostlight/src/consumer.rs +++ b/crates/ghostlight/src/consumer.rs @@ -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.v5" + "ghostlight.world_state.consumer.v6" ); } @@ -2295,4 +2295,224 @@ mod tests { assert!(!rendered.contains(SECRET), "{rendered}"); assert!(rendered.contains(""), "{rendered}"); } + + // ---- Cut 3: nobody but the play authority may rule or mint ---------- + + fn ruled_patch(handle: &str) -> WorldPatch { + WorldPatch { + declarations: vec![Declaration::Fact(FactDeclaration { + handle: DraftHandle::new(handle), + label: "The Ruling".into(), + statement: Statement::new("Stated by the table.").unwrap(), + standing: FactStandingRef::Ruled, + })], + operations: Vec::new(), + evidence: Vec::new(), + } + } + + fn mint_patch(mirror: &Mirror) -> WorldPatch { + WorldPatch { + declarations: Vec::new(), + operations: vec![ComponentOp::Mint { + holder: Ref::Existing(mirror.first), + resource: Ref::Existing(mirror.grain), + qty: Quantity(3), + }], + evidence: Vec::new(), + } + } + + fn elaborator_over(mirror: &Mirror) -> CallerId { + CallerId::System(SystemCapability::Elaborator { + jurisdiction: JurisdictionKey::PlaceSubtree(mirror.commons), + }) + } + + /// P3.2: the owner, seed, elaborators and consumers are refused a `Ruled` + /// fact both at `reduce` and (transitively, since the mailbox always + /// applies through `apply_effect`) at commit. Table-driven over the four + /// non-`Play` authors the map names. + #[test] + fn no_one_else_may_rule_a_fact() { + let refused_at_declaration = |error: KernelError, handle: &str| { + assert!( + matches!(&error, KernelError::PatchRejected(set) + if set.contains(&Mismatch::RuledWithoutAuthority { + site: patch::Site::Declaration(DraftHandle::new(handle)), + })), + "{error:?}" + ); + }; + + // The owner in Draft: the seed's caller. + { + let directory = tempfile::tempdir().unwrap(); + let (mut kernel, _mirror) = draft_kernel(directory.path(), "OwnerDraftRule"); + let snapshot = kernel.snapshot().unwrap(); + let error = kernel + .submit( + command( + &snapshot, + CommandId::new(), + CallerId::Principal(owner()), + CommandBody::AdmitPatch { + answers: None, + patch: ruled_patch("rule"), + }, + ), + &auth_principal(owner()), + ) + .unwrap_err(); + refused_at_declaration(error, "rule"); + } + + // The owner in Active, with a real answer. + { + let directory = tempfile::tempdir().unwrap(); + let (mut kernel, mirror) = mirror_kernel(directory.path(), "OwnerActiveRule"); + let answered = missing_structure(&kernel, mirror.first); + let snapshot = kernel.snapshot().unwrap(); + let error = kernel + .submit( + command( + &snapshot, + CommandId::new(), + CallerId::Principal(owner()), + CommandBody::AdmitPatch { + answers: Some(PatchAnswer::Boundary(answered)), + patch: ruled_patch("rule"), + }, + ), + &auth_principal(owner()), + ) + .unwrap_err(); + refused_at_declaration(error, "rule"); + } + + // An elaborator with a valid answer. + { + let directory = tempfile::tempdir().unwrap(); + let (mut kernel, mirror) = mirror_kernel(directory.path(), "ElaboratorRule"); + let answered = missing_structure(&kernel, mirror.first); + let error = submit_as( + &mut kernel, + elaborator_over(&mirror), + CommandBody::AdmitPatch { + answers: Some(PatchAnswer::Boundary(answered)), + patch: ruled_patch("rule"), + }, + ) + .unwrap_err(); + refused_at_declaration(error, "rule"); + } + + // A consumer on its own mirror. + { + let directory = tempfile::tempdir().unwrap(); + let (mut kernel, mirror) = mirror_kernel(directory.path(), "ConsumerRule"); + let answered = missing_structure(&kernel, mirror.first); + let error = as_consumer( + &mut kernel, + CommandBody::AdmitPatch { + answers: Some(PatchAnswer::Boundary(answered)), + patch: ruled_patch("rule"), + }, + ) + .unwrap_err(); + refused_at_declaration(error, "rule"); + } + } + + /// P3.2's other half: the same four callers may not mint. + #[test] + fn no_one_else_may_mint() { + let refused_at_op_zero = |error: KernelError| { + assert!( + matches!(&error, KernelError::PatchRejected(set) + if set.contains(&Mismatch::RuledWithoutAuthority { + site: patch::Site::Operation(0), + })), + "{error:?}" + ); + }; + + // The owner in Draft: the seed's caller. + { + let directory = tempfile::tempdir().unwrap(); + let (mut kernel, mirror) = draft_kernel(directory.path(), "OwnerDraftMint"); + let snapshot = kernel.snapshot().unwrap(); + let error = kernel + .submit( + command( + &snapshot, + CommandId::new(), + CallerId::Principal(owner()), + CommandBody::AdmitPatch { + answers: None, + patch: mint_patch(&mirror), + }, + ), + &auth_principal(owner()), + ) + .unwrap_err(); + refused_at_op_zero(error); + } + + // The owner in Active. + { + let directory = tempfile::tempdir().unwrap(); + let (mut kernel, mirror) = mirror_kernel(directory.path(), "OwnerActiveMint"); + let snapshot = kernel.snapshot().unwrap(); + let error = kernel + .submit( + command( + &snapshot, + CommandId::new(), + CallerId::Principal(owner()), + CommandBody::AdmitPatch { + answers: None, + patch: mint_patch(&mirror), + }, + ), + &auth_principal(owner()), + ) + .unwrap_err(); + refused_at_op_zero(error); + } + + // An elaborator with a valid answer: `require_patch_author` demands + // one for an elaborator whatever the patch declares. + { + let directory = tempfile::tempdir().unwrap(); + let (mut kernel, mirror) = mirror_kernel(directory.path(), "ElaboratorMint"); + let answered = missing_structure(&kernel, mirror.first); + let error = submit_as( + &mut kernel, + elaborator_over(&mirror), + CommandBody::AdmitPatch { + answers: Some(PatchAnswer::Boundary(answered)), + patch: mint_patch(&mirror), + }, + ) + .unwrap_err(); + refused_at_op_zero(error); + } + + // A consumer on its own mirror: an operations-only patch declares + // nothing, so no answer is needed to reach `require_ruler`. + { + let directory = tempfile::tempdir().unwrap(); + let (mut kernel, mirror) = mirror_kernel(directory.path(), "ConsumerMint"); + let error = as_consumer( + &mut kernel, + CommandBody::AdmitPatch { + answers: None, + patch: mint_patch(&mirror), + }, + ) + .unwrap_err(); + refused_at_op_zero(error); + } + } } diff --git a/crates/ghostlight/src/controllers.rs b/crates/ghostlight/src/controllers.rs index 4493090..4f5032c 100644 --- a/crates/ghostlight/src/controllers.rs +++ b/crates/ghostlight/src/controllers.rs @@ -76,8 +76,8 @@ pub(super) const CELL_TOOL_STEP_BUDGET: usize = 2; /// carried by tool identity, never by a model-written argument. const HANDLE_SEPARATOR: &str = "__"; const PERSONA_WORD_BUDGET: usize = 180; -const CONTROLLER_WORK_ROW: &str = "controller_work.v16"; -const CONTROLLER_WORK_SCHEMA: &str = "ghostlight.controller_work.v16"; +const CONTROLLER_WORK_ROW: &str = "controller_work.v17"; +const CONTROLLER_WORK_SCHEMA: &str = "ghostlight.controller_work.v17"; /// The Interpreter's byte-span capture tool. It is not the generated `speak` /// affordance tool: one captures an utterance out of preserved prose, the other diff --git a/crates/ghostlight/src/journal.rs b/crates/ghostlight/src/journal.rs index e71d6dc..a32edc4 100644 --- a/crates/ghostlight/src/journal.rs +++ b/crates/ghostlight/src/journal.rs @@ -584,6 +584,7 @@ pub(super) fn verify_state_shape(state: &WorldState) -> Result<(), JournalError> super::patch::is_canonical_text(evidence.text()) } super::FactStanding::Claimed { by } => state.subjects.contains_key(by), + super::FactStanding::Ruled => true, }; if !state .entities diff --git a/crates/ghostlight/src/lib.rs b/crates/ghostlight/src/lib.rs index 242e952..65e8026 100644 --- a/crates/ghostlight/src/lib.rs +++ b/crates/ghostlight/src/lib.rs @@ -95,7 +95,7 @@ pub use elaboration::{ pub use lens::{Lens, LensWeights}; pub use local_inference::{DEFAULT_LOCAL_MODEL_PREFIX, LocalBinding}; pub(crate) use mailbox::ElaborationPort; -pub use mailbox::{ConsumerPort, ControllerPort, MailboxError, SeedPort, WorldMailbox}; +pub use mailbox::{ConsumerPort, ControllerPort, MailboxError, PlayPort, SeedPort, WorldMailbox}; pub use patch::{ CommitmentKey, DraftHandle, JurisdictionKey, Mismatch, PatchAnswer, Ref, RefKind, RefName, Role, Site, Statement, WorldPatch, @@ -173,8 +173,8 @@ impl VerifiedPrincipalEvidence { } } -pub const STATE_SCHEMA: &str = "ghostlight.world_state.consumer.v5"; -pub(crate) const COMMIT_SCHEMA: &str = "ghostlight.world_commit.consumer.v5"; +pub const STATE_SCHEMA: &str = "ghostlight.world_state.consumer.v6"; +pub(crate) const COMMIT_SCHEMA: &str = "ghostlight.world_commit.consumer.v6"; /// Compatibility tag derived from [`STATE_SCHEMA`]: the trailing /// `-` pair (e.g. `foundation-v1`). Callers that publish a @@ -480,6 +480,10 @@ enum SystemCapability { Consumer { consumer: ConsumerId, }, + /// The play table. Admitted for unanswered, unconfined Active patches, the + /// only author of `Ruled` facts and `Mint`, and for the clock. Minted only + /// by `PlayPort`. + Play, } /// What a confined patch author may write. `None` from `require_patch_author` @@ -1664,12 +1668,14 @@ fn reduce(state: &WorldState, command: &CommandEnvelope) -> Result Result<() let admitted = matches!( (capability, body), (SystemCapability::Clock, CommandBody::AdvanceTime { .. }) + | (SystemCapability::Play, CommandBody::AdvanceTime { .. }) | ( - SystemCapability::Elaborator { .. } | SystemCapability::Consumer { .. }, + SystemCapability::Elaborator { .. } + | SystemCapability::Consumer { .. } + | SystemCapability::Play, CommandBody::AdmitPatch { .. } ) ); @@ -1725,7 +1734,10 @@ fn require_system_capability(caller: &CallerId, body: &CommandBody) -> Result<() /// session. Nothing /// else may tick. fn require_clock_caller(state: &WorldState, caller: &CallerId) -> Result<(), KernelError> { - if caller == &CallerId::System(SystemCapability::Clock) { + if matches!( + caller, + CallerId::System(SystemCapability::Clock) | CallerId::System(SystemCapability::Play) + ) { Ok(()) } else { require_owner(state, caller) @@ -1741,13 +1753,26 @@ fn require_clock_caller(state: &WorldState, caller: &CallerId) -> Result<(), Ker /// jurisdiction whose deficit is nonzero. fn require_answer( state: &WorldState, + caller: &CallerId, answers: Option<&PatchAnswer>, declares: bool, ) -> Result<(), KernelError> { + if caller == &CallerId::System(SystemCapability::Play) && state.phase == WorldPhase::Draft { + return Err(KernelError::WrongPhase { + expected: WorldPhase::Active, + actual: state.phase, + }); + } match (state.phase, answers) { (WorldPhase::Draft, None) => Ok(()), (WorldPhase::Draft, Some(_)) => Err(KernelError::AnswerNotDerived), - (WorldPhase::Active, None) if declares => Err(KernelError::AnswerRequired), + (WorldPhase::Active, None) if declares => { + if caller == &CallerId::System(SystemCapability::Play) { + Ok(()) + } else { + Err(KernelError::AnswerRequired) + } + } (WorldPhase::Active, None) => Ok(()), (WorldPhase::Active, Some(PatchAnswer::Boundary(claimed))) => { exact_boundary(state, claimed).map(|_| ()) @@ -2050,6 +2075,7 @@ fn admit_resolved(state: &mut WorldState, resolved: &ResolvedPatch) -> Result<() !(patch::is_canonical_text(evidence.text()) && resolved.evidence.contains(evidence)) } FactStanding::Claimed { by } => !state.subjects.contains_key(by), + FactStanding::Ruled => false, }; if !patch::is_canonical_text(&declared.entity.label) || !patch::is_canonical_text(declared.fact.statement.as_str()) @@ -2143,6 +2169,9 @@ fn apply_operations( ResolvedOp::Admit { resource, qty, .. } => { deltas.entry(*resource).or_default().admitted += u128::from(qty.0); } + ResolvedOp::Mint { resource, qty, .. } => { + deltas.entry(*resource).or_default().admitted += u128::from(qty.0); + } _ => {} } } @@ -2177,7 +2206,8 @@ fn operation_resources(operation: &ResolvedOp) -> Vec { match operation { ResolvedOp::Transfer { resource, .. } | ResolvedOp::Consume { resource, .. } - | ResolvedOp::Admit { resource, .. } => vec![*resource], + | ResolvedOp::Admit { resource, .. } + | ResolvedOp::Mint { resource, .. } => vec![*resource], ResolvedOp::Transform { from_resource, into_resource, @@ -2394,6 +2424,22 @@ fn apply_operation( .ok_or_else(overflow)?; set_held(state, *holder, *resource, admitted); } + ResolvedOp::Mint { + holder, + resource, + qty, + } => { + if !custody_referents_exist(state, *holder, *resource) { + return Err(unknown()); + } + if qty.0 == 0 { + return Err(zero()); + } + let minted = held(state, *holder, *resource) + .checked_add(qty.0) + .ok_or_else(overflow)?; + set_held(state, *holder, *resource, minted); + } ResolvedOp::Bind { subject, target } | ResolvedOp::Release { subject, target } => { let bind = matches!(operation, ResolvedOp::Bind { .. }); if !state.subjects.contains_key(subject) || !dependency_target_exists(state, *target) { @@ -3662,6 +3708,7 @@ fn snapshot(state: &WorldState) -> Result { standing: match &record.standing { FactStanding::Canonical { .. } => FactStandingView::Canonical, FactStanding::Claimed { by } => FactStandingView::Claimed { by: *by }, + FactStanding::Ruled => FactStandingView::Canonical, }, confidence: held.confidence, source: held.source, @@ -4290,6 +4337,7 @@ fn require_patch_author( ) -> Result, KernelError> { match caller { CallerId::Principal(principal) if principal == &state.owner => Ok(None), + CallerId::System(SystemCapability::Play) => Ok(None), CallerId::System(SystemCapability::Elaborator { jurisdiction }) => { let answer = answers.ok_or(KernelError::Unauthorized)?; let ground = PatchGround::Jurisdiction(*jurisdiction); @@ -4514,6 +4562,11 @@ fn confine_to_ground( mismatches.push(Mismatch::OutsideJurisdiction { site }); } } + // Redundant with `require_ruler`, which refuses a confined caller's + // `Ruled` fact outright; kept so this match stays total. + FactStanding::Ruled => { + mismatches.push(Mismatch::OutsideJurisdiction { site }); + } } } for declared in &resolved.channels { @@ -4558,6 +4611,38 @@ fn confine_to_ground( } } +/// The one author of a `Ruled` fact and a `Mint`. `Ok` when the caller is +/// `System(Play)`. Otherwise, one `Mismatch::RuledWithoutAuthority` per +/// `Ruled` fact and per `Mint`, and `Ok` if there are none. Called in +/// `reduce`'s `AdmitPatch` arm right after `resolve_patch` and before +/// confinement, and re-decided in `apply_effect`'s `PatchAdmitted` arm. The +/// resolver stays actor-blind: this runs after resolution, never inside it. +fn require_ruler(caller: &CallerId, resolved: &ResolvedPatch) -> Result<(), Vec> { + if caller == &CallerId::System(SystemCapability::Play) { + return Ok(()); + } + let mut mismatches = Vec::new(); + for declared in &resolved.facts { + if matches!(declared.fact.standing, FactStanding::Ruled) { + mismatches.push(Mismatch::RuledWithoutAuthority { + site: Site::Declaration(declared.handle.clone()), + }); + } + } + for (index, operation) in resolved.operations.iter().enumerate() { + if matches!(operation, ResolvedOp::Mint { .. }) { + mismatches.push(Mismatch::RuledWithoutAuthority { + site: Site::Operation(index), + }); + } + } + if mismatches.is_empty() { + Ok(()) + } else { + Err(mismatches) + } +} + /// The ground one lowered operation touches: the subjects whose position it /// must sit under, the places it names directly, and the routes whose endpoints /// it must sit under. Total over `ResolvedOp`, so a new operation cannot ship @@ -4605,6 +4690,9 @@ fn operation_ground( } | ResolvedOp::Admit { holder, resource, .. + } + | ResolvedOp::Mint { + holder, resource, .. } => (vec![*holder], places(vec![*resource]), Vec::new()), ResolvedOp::Bind { subject, target } | ResolvedOp::Release { subject, target } => { match target { @@ -4774,8 +4862,11 @@ fn apply_effect( let admitted = matches!( (capability, effect), (SystemCapability::Clock, WorldEffect::TimeAdvanced { .. }) + | (SystemCapability::Play, WorldEffect::TimeAdvanced { .. }) | ( - SystemCapability::Elaborator { .. } | SystemCapability::Consumer { .. }, + SystemCapability::Elaborator { .. } + | SystemCapability::Consumer { .. } + | SystemCapability::Play, WorldEffect::PatchAdmitted { .. } ) ); @@ -4790,7 +4881,12 @@ fn apply_effect( )); } WorldEffect::PatchAdmitted { answers, resolved } => { - require_answer(state, answers.as_ref(), !resolved.declares_nothing())?; + require_answer(state, caller, answers.as_ref(), !resolved.declares_nothing())?; + require_ruler(caller, resolved).map_err(|_| { + KernelError::Invariant( + "admitted patch rules or mints without the play authority".into(), + ) + })?; let confinement = require_patch_author(state, caller, answers.as_ref()).map_err(|_| { KernelError::Invariant( @@ -15830,4 +15926,472 @@ mod clock_tests { ); assert_eq!(kernel.state.revision, revision); } + + // ---- Cut 3: the play authority, `Ruled` facts and `Mint` ----------- + + fn play_caller() -> CallerId { + CallerId::System(SystemCapability::Play) + } + + fn ruled_fact_patch(handle: &str) -> WorldPatch { + WorldPatch { + declarations: vec![Declaration::Fact(FactDeclaration { + handle: DraftHandle::new(handle), + label: "The Ruling".into(), + statement: Statement::new("Stated by the table.").unwrap(), + standing: FactStandingRef::Ruled, + })], + operations: Vec::new(), + evidence: Vec::new(), + } + } + + fn resource_named(kernel: &WorldKernel, label: &str) -> EntityId { + *kernel + .state + .entities + .iter() + .find(|(_, record)| record.label == label) + .map(|(id, _)| id) + .expect("the declared resource") + } + + /// Invariant 5, first half, and P3.1: the play authority declares in + /// Active with no answer. + #[test] + fn the_play_authority_declares_in_active_without_an_answer() { + let directory = tempfile::tempdir().unwrap(); + let (mut kernel, clockwork, _) = clock_kernel(directory.path(), "PlayDeclares"); + let receipt = submit_as( + &mut kernel, + play_caller(), + CommandBody::AdmitPatch { + answers: None, + patch: shed_under(clockwork.dead_end, "play-shed"), + }, + ) + .expect("the play authority declares unanswered in Active"); + assert!(matches!(receipt, SubmitReceipt::Applied(_))); + } + + /// Invariant 5, second half: the elaborator gate is unchanged. + #[test] + fn an_elaborator_declaration_in_active_without_an_answer_is_still_refused() { + let directory = tempfile::tempdir().unwrap(); + let (mut kernel, clockwork, _) = clock_kernel(directory.path(), "ElaboratorUnanswered"); + let error = submit_as( + &mut kernel, + elaborator(JurisdictionKey::PlaceSubtree(clockwork.dead_end)), + CommandBody::AdmitPatch { + answers: None, + patch: shed_under(clockwork.dead_end, "shed"), + }, + ) + .unwrap_err(); + assert!(matches!(error, KernelError::AnswerRequired), "{error:?}"); + } + + /// Invariant 5, second half: the owner's own gate is unchanged too. Only + /// `Play` is exempted from `AnswerRequired`. + #[test] + fn the_owner_still_cannot_declare_unanswered_in_active() { + let directory = tempfile::tempdir().unwrap(); + let (mut kernel, clockwork, _) = clock_kernel(directory.path(), "OwnerUnanswered"); + let snapshot = kernel.snapshot().unwrap(); + let error = kernel + .submit( + command( + &snapshot, + CommandId::new(), + CallerId::Principal(owner()), + CommandBody::AdmitPatch { + answers: None, + patch: shed_under(clockwork.dead_end, "shed"), + }, + ), + &auth_principal(owner()), + ) + .unwrap_err(); + assert!(matches!(error, KernelError::AnswerRequired), "{error:?}"); + } + + /// Invariant 5: the play authority reaches the whole world, unconfined to + /// a ground. Writing under a place no jurisdiction covers, with no + /// answer, is exactly what an elaborator or the owner cannot do. + #[test] + fn the_play_authority_is_unconfined() { + let directory = tempfile::tempdir().unwrap(); + let (mut kernel, clockwork, _) = clock_kernel(directory.path(), "PlayUnconfined"); + submit_as( + &mut kernel, + play_caller(), + CommandBody::AdmitPatch { + answers: None, + patch: shed_under(clockwork.dead_end, "unconfined-shed"), + }, + ) + .expect("the play authority is confined to no ground"); + } + + /// P3.6: the play authority cannot act in Draft. + #[test] + fn the_play_authority_is_refused_in_draft() { + let directory = tempfile::tempdir().unwrap(); + let mut kernel = WorldKernel::create( + directory.path().join("world.cc"), + creation(CommandId::new(), "PlayDraft"), + &auth_principal(owner()), + ) + .expect("a created world") + .0; + let commons = kernel.snapshot().unwrap().places[0].id; + let error = submit_as( + &mut kernel, + play_caller(), + CommandBody::AdmitPatch { + answers: None, + patch: shed_under(commons, "shed"), + }, + ) + .unwrap_err(); + assert!( + matches!( + error, + KernelError::WrongPhase { + expected: WorldPhase::Active, + actual: WorldPhase::Draft, + } + ), + "{error:?}" + ); + } + + /// Invariant 5: the play authority advances time, the clock's precedent. + #[test] + fn the_play_authority_advances_time() { + let directory = tempfile::tempdir().unwrap(); + let (mut kernel, _clockwork, active) = clock_kernel(directory.path(), "PlayClock"); + let receipt = submit_as( + &mut kernel, + play_caller(), + CommandBody::AdvanceTime { + minutes: minutes(30), + }, + ) + .expect("the play authority advances time"); + assert!(matches!(receipt, SubmitReceipt::Applied(_))); + assert!(kernel.state.now > active.now); + } + + /// P3.6: the play authority may not approve, activate, exercise, or + /// decline. `require_system_capability`, at the top of `reduce`, refuses + /// all four before any body-specific arm is read. + #[test] + fn the_play_authority_cannot_approve_activate_exercise_or_decline() { + let directory = tempfile::tempdir().unwrap(); + let (mut kernel, clockwork, active) = clock_kernel(directory.path(), "PlayForbidden"); + let opportunity = opportunity_for(&active, clockwork.reeve); + let forbidden = [ + CommandBody::ApproveDraft, + CommandBody::ActivateWorld, + CommandBody::ExerciseDecision { + opportunity: opportunity.clone(), + invocation: DecisionInvocation { + affordance: clockwork.deliver, + bindings: Vec::new(), + proposed: Vec::new(), + speech: None, + display: None, + }, + }, + CommandBody::DeclineDecision { + opportunity: opportunity.clone(), + }, + ]; + for body in &forbidden { + let error = submit_as(&mut kernel, play_caller(), body.clone()).unwrap_err(); + assert!(matches!(error, KernelError::Unauthorized), "{error:?}"); + } + // The end-to-end check above cannot tell "refused before any + // body-specific arm is read" apart from a downstream refusal that + // would fail Play just as closed (`require_owner`, for one, refuses + // every non-owner caller regardless of what admitted it). Pin + // `require_system_capability` directly, so widening its table for + // `Play` is caught even though every arm behind it still happens to + // refuse `Play` on its own. + for body in &forbidden { + assert!( + matches!( + require_system_capability(&play_caller(), body), + Err(KernelError::Unauthorized) + ), + "require_system_capability admitted {body:?} for the play authority" + ); + } + } + + /// P3.4: a ruled fact is world truth to every subject that knows it. + #[test] + fn the_play_authority_rules_a_fact_and_a_witness_knows_it_as_canon() { + let directory = tempfile::tempdir().unwrap(); + let (mut kernel, clockwork, _) = clock_kernel(directory.path(), "PlayRules"); + let mut patch = ruled_fact_patch("verdict"); + patch.operations.push(ComponentOp::AcquireKnowledge { + subject: Ref::Existing(clockwork.reeve), + fact: Ref::Draft(DraftHandle::new("verdict")), + source: AuthoredSource::Witnessed, + confidence: Confidence::Certain, + }); + submit_as( + &mut kernel, + play_caller(), + CommandBody::AdmitPatch { + answers: None, + patch, + }, + ) + .expect("the play authority rules a fact and grants it as knowledge"); + + let fact = *kernel + .state + .facts + .iter() + .find(|(_, record)| record.statement.as_str() == "Stated by the table.") + .map(|(id, _)| id) + .expect("the ruled fact"); + assert_eq!(kernel.state.facts[&fact].standing, FactStanding::Ruled); + + let snapshot = kernel.snapshot().unwrap(); + let subject = snapshot + .subjects + .iter() + .find(|subject| subject.id == clockwork.reeve) + .expect("the reeve's snapshot"); + let known = subject + .knowledge + .iter() + .find(|held| held.fact == fact) + .expect("the reeve knows the ruled fact"); + assert_eq!(known.standing, FactStandingView::Canonical); + } + + /// P3.5: minted quantity balances the ledger, and replay reproduces it. + #[test] + fn the_play_authority_mints_and_the_ledger_balances() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("world.cc"); + let (world_id, reeve, grain) = { + let (mut kernel, clockwork, _) = clock_kernel(directory.path(), "PlayMints"); + let grain = resource_named(&kernel, "Winter Grain"); + let before = held(&kernel.state, clockwork.reeve, grain); + assert_eq!(before, 0); + submit_as( + &mut kernel, + play_caller(), + CommandBody::AdmitPatch { + answers: None, + patch: WorldPatch { + declarations: Vec::new(), + operations: vec![ComponentOp::Mint { + holder: Ref::Existing(clockwork.reeve), + resource: Ref::Existing(grain), + qty: Quantity(7), + }], + evidence: Vec::new(), + }, + }, + ) + .expect("the play authority mints"); + let after = held(&kernel.state, clockwork.reeve, grain); + assert_eq!(before + 7, after); + (kernel.state.world_id, clockwork.reeve, grain) + }; + let replayed = WorldKernel::open(&path, world_id).expect("the minted store replays"); + assert_eq!(held(&replayed.state, reeve, grain), 7); + } + + /// The evidenced-knowledge clause is unchanged: a receipt cannot vouch for + /// an assertion the kernel never evaluated, and a `Ruled` fact is exactly + /// such an assertion. + #[test] + fn a_ruled_fact_cannot_be_known_evidenced() { + let directory = tempfile::tempdir().unwrap(); + let (mut kernel, clockwork, _) = clock_kernel(directory.path(), "PlayEvidenced"); + let mut patch = ruled_fact_patch("verdict"); + patch.operations.push(ComponentOp::AcquireKnowledge { + subject: Ref::Existing(clockwork.reeve), + fact: Ref::Draft(DraftHandle::new("verdict")), + source: AuthoredSource::Evidenced, + confidence: Confidence::Certain, + }); + let error = submit_as( + &mut kernel, + play_caller(), + CommandBody::AdmitPatch { + answers: None, + patch, + }, + ) + .unwrap_err(); + assert!( + matches!(&error, KernelError::PatchRejected(set) + if set.contains(&Mismatch::EvidencedKnowledgeOfClaim { operation: 0 })), + "{error:?}" + ); + } + + /// The evidence gate on `Canonical` facts and `Admit` is byte-for-byte the + /// same predicate at the same sites for every author, `Play` included + /// (P3.3). `Play` carries no receipts, so its `Admit` is refused like + /// anyone's without one. + #[test] + fn the_evidence_gate_is_unchanged_for_every_author() { + let directory = tempfile::tempdir().unwrap(); + let (mut kernel, clockwork, _) = clock_kernel(directory.path(), "PlayEvidenceGate"); + let grain = resource_named(&kernel, "Winter Grain"); + + // Without a receipt: `Play`'s `Admit` is refused exactly as anyone's. + let error = submit_as( + &mut kernel, + play_caller(), + CommandBody::AdmitPatch { + answers: None, + patch: WorldPatch { + declarations: Vec::new(), + operations: vec![ComponentOp::Admit { + holder: Ref::Existing(clockwork.reeve), + resource: Ref::Existing(grain), + qty: Quantity(1), + evidence: EvidenceRef::new("vault:uncited"), + }], + evidence: Vec::new(), + }, + }, + ) + .unwrap_err(); + assert!( + matches!(&error, KernelError::PatchRejected(set) + if set.contains(&Mismatch::AdmitWithoutEvidence { operation: 0 })), + "{error:?}" + ); + + // With the receipt cited in the same patch, `Play`'s `Admit` is + // admitted like anyone unconfined's. + submit_as( + &mut kernel, + play_caller(), + CommandBody::AdmitPatch { + answers: None, + patch: WorldPatch { + declarations: Vec::new(), + operations: vec![ComponentOp::Admit { + holder: Ref::Existing(clockwork.reeve), + resource: Ref::Existing(grain), + qty: Quantity(1), + evidence: EvidenceRef::new("vault:cited"), + }], + evidence: vec![EvidenceRef::new("vault:cited")], + }, + }, + ) + .expect("an evidenced admit is admitted for the play authority too"); + } + + /// `reduce` decides and `apply_effect` re-decides: a forged + /// `PatchAdmitted` under an elaborator caller, carrying a `Mint` the + /// resolver would accept (the resolver is actor-blind) and that fully + /// satisfies confinement (the minted subject is declared inside the + /// elaborator's own jurisdiction), is still refused at apply. `Mint` + /// carries no confinement veto of its own — unlike a `Ruled` fact, + /// `confine_to_ground` has no redundant clause for it — so only + /// `require_ruler`'s re-decision can be refusing this. Follows + /// `apply_effect_re_decides_elaborator_authority`. + #[test] + fn apply_effect_re_decides_the_ruler() { + let directory = tempfile::tempdir().unwrap(); + let (kernel, clockwork, _) = clock_kernel(directory.path(), "RedecideRulerMint"); + let answered = dead_end_boundary(&kernel); + let speak = super::tests::speak_entry(&kernel); + let command_id = CommandId::issue(); + let patch = WorldPatch { + declarations: vec![ + Declaration::Entity(EntityDeclaration { + handle: DraftHandle::new("loot"), + label: "Roadside Loot".into(), + kind: EntityKind::Resource, + container: None, + }), + Declaration::Subject(SubjectDeclaration { + handle: DraftHandle::new("finder"), + label: "The Roadside Finder".into(), + kind: SubjectKind::Person, + controller: NewController::NarrativePersona, + affordances: BTreeSet::from([speak]), + position: Some(Ref::Existing(clockwork.dead_end)), + }), + ], + operations: vec![ComponentOp::Mint { + holder: Ref::Draft(DraftHandle::new("finder")), + resource: Ref::Draft(DraftHandle::new("loot")), + qty: Quantity(4), + }], + evidence: Vec::new(), + }; + let resolved = patch::resolve_patch(&kernel.state, command_id, &patch, None, None) + .expect("the patch resolves; the resolver is actor-blind"); + let effect = WorldEffect::PatchAdmitted { + answers: Some(PatchAnswer::Boundary(answered)), + resolved, + }; + + let mut candidate = kernel.state.clone(); + let error = apply_effect( + &mut candidate, + command_id, + &elaborator(JurisdictionKey::PlaceSubtree(clockwork.dead_end)), + &effect, + ) + .unwrap_err(); + assert!(matches!(error, KernelError::Invariant(_)), "{error:?}"); + assert_eq!(candidate, kernel.state); + + let mut candidate = kernel.state.clone(); + apply_effect(&mut candidate, command_id, &play_caller(), &effect) + .expect("the play authority applies through the same arm"); + assert_ne!(candidate, kernel.state); + } + + /// P3.5, replay half, folded with a ruled fact: reopening the journal + /// reproduces both across the digest. + #[test] + fn a_journal_holding_a_ruled_fact_and_a_mint_replays() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("world.cc"); + let (world_id, expected) = { + let (mut kernel, clockwork, _) = clock_kernel(directory.path(), "PlayReplay"); + let grain = resource_named(&kernel, "Winter Grain"); + let mut patch = ruled_fact_patch("verdict"); + patch.operations.push(ComponentOp::Mint { + holder: Ref::Existing(clockwork.farmer), + resource: Ref::Existing(grain), + qty: Quantity(2), + }); + submit_as( + &mut kernel, + play_caller(), + CommandBody::AdmitPatch { + answers: None, + patch, + }, + ) + .expect("the play authority rules and mints in one patch"); + (kernel.state.world_id, kernel.snapshot().unwrap()) + }; + let replayed = WorldKernel::open(&path, world_id).expect("the store replays"); + assert_eq!(replayed.snapshot().unwrap(), expected); + drop(replayed); + let replayed_again = + WorldKernel::open(&path, world_id).expect("the store replays a second time"); + assert_eq!(replayed_again.snapshot().unwrap(), expected); + } } diff --git a/crates/ghostlight/src/mailbox.rs b/crates/ghostlight/src/mailbox.rs index 84f04af..3593348 100644 --- a/crates/ghostlight/src/mailbox.rs +++ b/crates/ghostlight/src/mailbox.rs @@ -648,6 +648,59 @@ impl SeedPort { } } +/// The play table's one door. Minted only by Dungeon, which constructs one +/// `PlayPort` in its `AppState`; exclusivity is that single construction site, +/// pinned by a negative grep, exactly as every other system capability's is. +/// No session evidence backs it: it is a system capability like the clock, +/// not a principal, and it carries no expiry to hold. +#[derive(Clone)] +pub struct PlayPort { + mailbox: WorldMailbox, +} + +impl PlayPort { + pub fn new(mailbox: WorldMailbox) -> Self { + Self { mailbox } + } + + pub async fn snapshot(&self) -> Result { + self.mailbox.snapshot().await + } + + pub async fn submit_patch( + &self, + command_id: CommandId, + patch: WorldPatch, + ) -> Result { + self.mailbox + .submit_stamped( + command_id, + CallerId::System(SystemCapability::Play), + CommandBody::AdmitPatch { + answers: None, + patch, + }, + AuthenticatedCaller::verified_system(SystemCapability::Play), + ) + .await + } + + pub async fn advance_time( + &self, + command_id: CommandId, + minutes: TickMinutes, + ) -> Result { + self.mailbox + .submit_stamped( + command_id, + CallerId::System(SystemCapability::Play), + CommandBody::AdvanceTime { minutes }, + AuthenticatedCaller::verified_system(SystemCapability::Play), + ) + .await + } +} + /// The consumer ingress's narrowing of the mailbox: one method. The ingress /// has no reason to read the world — it selects no answer, pre-validates /// nothing, and returns a receipt rather than state — so a snapshot method here diff --git a/crates/ghostlight/src/patch.rs b/crates/ghostlight/src/patch.rs index d4406b5..2e0bf89 100644 --- a/crates/ghostlight/src/patch.rs +++ b/crates/ghostlight/src/patch.rs @@ -300,6 +300,9 @@ pub(crate) enum FactStanding { Canonical { evidence: EvidenceRef }, /// Asserted by a subject. The kernel does not evaluate the assertion. Claimed { by: SubjectId }, + /// Stated by the play table. The kernel does not evaluate it; only `Play` + /// may write it. + Ruled, } /// The proposal-time twin of [`FactStanding`]. @@ -308,6 +311,7 @@ pub(crate) enum FactStanding { pub(crate) enum FactStandingRef { Canonical { evidence: EvidenceRef }, Claimed { by: Ref }, + Ruled, } /// One `EntityKind::Fact` row's payload. Write-once: only declaration and @@ -1058,6 +1062,13 @@ pub(crate) enum ComponentOp { qty: Quantity, evidence: EvidenceRef, }, + /// Creates quantity with no receipt. Only `Play` may write it; every other + /// author creates quantity through `Admit` and its evidence. + Mint { + holder: Ref, + resource: Ref, + qty: Quantity, + }, Bind { subject: Ref, target: DependencyRef, @@ -1560,6 +1571,11 @@ pub enum Mismatch { OutsideJurisdiction { site: Site, }, + /// A `Ruled` fact or a `Mint` was written by a caller other than the play + /// authority. + RuledWithoutAuthority { + site: Site, + }, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] @@ -1724,6 +1740,11 @@ pub(crate) enum ResolvedOp { qty: Quantity, evidence: EvidenceRef, }, + Mint { + holder: SubjectId, + resource: EntityId, + qty: Quantity, + }, Bind { subject: SubjectId, target: DependencyTarget, @@ -2111,6 +2132,7 @@ fn candidate_effective_authority( enum FactCandidate { Canonical, Claimed(Key), + Ruled, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -3183,6 +3205,7 @@ pub(super) fn resolve_patch( match &record.standing { FactStanding::Canonical { .. } => FactCandidate::Canonical, FactStanding::Claimed { by } => FactCandidate::Claimed(Key::Existing(*by)), + FactStanding::Ruled => FactCandidate::Ruled, }, ) }) @@ -3346,6 +3369,7 @@ pub(super) fn resolve_patch( &mut mismatches, ) .map(FactCandidate::Claimed), + FactStandingRef::Ruled => Some(FactCandidate::Ruled), }; if let Some(standing) = standing { facts.insert(Key::Draft(fact.handle.clone()), standing); @@ -3959,6 +3983,48 @@ pub(super) fn resolve_patch( candidate_set(&mut holdings, &holder_key, &resource_key, admitted); deltas.entry(resource_key).or_default().admitted += u128::from(qty.0); } + ComponentOp::Mint { + holder, + resource, + qty, + } => { + let holder_key = resolve_subject( + Site::Operation(position), + holder, + &index, + &state.subjects, + &mut mismatches, + ); + let resource_key = resolve_entity( + Site::Operation(position), + EntityKind::Resource, + resource, + &index, + &state.entities, + &mut mismatches, + ); + if qty.0 == 0 { + mismatches.push(Mismatch::ZeroQuantity { + operation: position, + }); + } + let (Some(holder_key), Some(resource_key)) = (holder_key, resource_key) else { + continue; + }; + if qty.0 == 0 { + continue; + } + let Some(minted) = + candidate_held(&holdings, &holder_key, &resource_key).checked_add(qty.0) + else { + mismatches.push(Mismatch::QuantityOverflow { + operation: position, + }); + continue; + }; + candidate_set(&mut holdings, &holder_key, &resource_key, minted); + deltas.entry(resource_key).or_default().admitted += u128::from(qty.0); + } ComponentOp::Bind { subject, target } | ComponentOp::Release { subject, target } => { let bind = matches!(operation, ComponentOp::Bind { .. }); let subject_key = resolve_subject( @@ -4968,6 +5034,7 @@ pub(super) fn resolve_patch( FactStandingRef::Claimed { by } => FactStanding::Claimed { by: subject_id_of(&key_of(by)), }, + FactStandingRef::Ruled => FactStanding::Ruled, }, }, }), @@ -5146,6 +5213,15 @@ pub(super) fn resolve_patch( qty: *qty, evidence: evidence.clone(), }, + ComponentOp::Mint { + holder, + resource, + qty, + } => ResolvedOp::Mint { + holder: subject_id_of(&key_of(holder)), + resource: entity_id_of(&key_of(resource)), + qty: *qty, + }, ComponentOp::Bind { subject, target } => ResolvedOp::Bind { subject: subject_id_of(&key_of(subject)), target: target_of(target), @@ -5692,7 +5768,7 @@ pub(crate) const PATCH_TOOLS: &[PatchTool] = &[ }, PatchTool { name: "declare_fact", - description: "Declare a fact: a short authored name and the statement it carries.", + description: "Declare a fact: a short authored name and the statement it carries. Only the play table may rule a fact.", fields: &[ field("handle", PatchFieldKind::Handle), field("label", PatchFieldKind::Label), @@ -5806,6 +5882,16 @@ pub(crate) const PATCH_TOOLS: &[PatchTool] = &[ ], shape: PatchToolShape::Operate { variant: "admit" }, }, + PatchTool { + name: "mint", + description: "Create quantity with no receipt. Only the play table may mint.", + fields: &[ + field("holder", PatchFieldKind::Reference("subject")), + field("resource", PatchFieldKind::Reference("resource")), + field("qty", PatchFieldKind::Quantity), + ], + shape: PatchToolShape::Operate { variant: "mint" }, + }, PatchTool { name: "bind", description: "Record that a subject depends on a resource, a route, or another subject.", @@ -6278,6 +6364,7 @@ fn composite_schema(shape: CompositeShape) -> Value { )], ), ("claimed", vec![("by".to_owned(), subject_ref_schema())]), + ("ruled", vec![]), ], ), CompositeShape::AudienceRef => tool_schema::external_variant(vec![ @@ -6848,7 +6935,7 @@ mod catalog_tests { .iter() .filter(|entry| matches!(entry.shape, PatchToolShape::Operate { .. })) .count(); - assert_eq!((declarations, operations, PATCH_TOOLS.len()), (7, 30, 39)); + assert_eq!((declarations, operations, PATCH_TOOLS.len()), (7, 31, 40)); // Every declaration variant the vocabulary owns is reachable, and the // two payload-carrying entity kinds are not exposed as an `Entity` @@ -6920,6 +7007,7 @@ mod catalog_tests { ComponentOp::Transform { .. } => "transform", ComponentOp::Consume { .. } => "consume", ComponentOp::Admit { .. } => "admit", + ComponentOp::Mint { .. } => "mint", ComponentOp::Bind { .. } => "bind", ComponentOp::Release { .. } => "release", ComponentOp::GrantAuthority { .. } => "grant_authority", @@ -6993,7 +7081,7 @@ mod catalog_tests { .count(); assert_eq!( (declare_tools, operate_tools, PATCH_TOOLS.len()), - (7, 30, 39) + (7, 31, 40) ); let declarations = every_declaration(); diff --git a/crates/ghostlight/tests/external_admission.rs b/crates/ghostlight/tests/external_admission.rs index 857c2c4..0ce24bf 100644 --- a/crates/ghostlight/tests/external_admission.rs +++ b/crates/ghostlight/tests/external_admission.rs @@ -19,9 +19,9 @@ use chrono::{Duration, Utc}; use ghostlight::{ CommandBody, CommandId, ControllerMode, ControllerPort, CreateWorldIntent, DecisionInvocation, - DecisionOpportunity, KernelError, Lens, LensWeights, MailboxError, Mismatch, PrincipalCommandIntent, - Statement, SubjectId, SubmitReceipt, VerifiedPrincipalEvidence, WorldMailbox, WorldPhase, - WorldSnapshot, + DecisionOpportunity, KernelError, Lens, LensWeights, MailboxError, Mismatch, PlayPort, + PrincipalCommandIntent, Statement, SubjectId, SubmitReceipt, VerifiedPrincipalEvidence, + WorldMailbox, WorldPatch, WorldPhase, WorldSnapshot, }; use serde_json::{Value, json}; use std::collections::BTreeMap; @@ -693,6 +693,50 @@ async fn a_lens_set_spelled_with_or_without_its_zero_weights_commits_nothing() { assert_eq!(world.snapshot().await.lens_weights, moved); } +/// Cut 3: `PlayPort` is the play authority's only door, and it is a public, +/// externally constructible type — Dungeon mints exactly one of these in its +/// `AppState`. This is a compile-pass use of the port from outside the crate: +/// it mints quantity through `Mint`, the spelling only `Play` may write, on a +/// resource it declares in the same patch, unconfined and with no answer. +#[tokio::test] +async fn play_port_is_the_only_play_minter() { + let world = World::active().await; + let play = PlayPort::new(world.mailbox.clone()); + let holder = world + .snapshot() + .await + .subjects + .first() + .expect("genesis declares at least one subject") + .id; + let patch: WorldPatch = serde_json::from_value(json!({ + "declarations": [ + { + "type": "entity", + "handle": "loot", + "label": "Ruled Loot", + "kind": "resource", + "container": null, + }, + ], + "operations": [ + { + "op": "mint", + "holder": {"ref": "existing", "value": holder}, + "resource": {"ref": "draft", "value": "loot"}, + "qty": 5, + }, + ], + "evidence": [], + })) + .expect("a mint operation, off the wire, decodes into a WorldPatch"); + let receipt = play + .submit_patch(CommandId::new(), patch) + .await + .expect("the play authority mints with no answer, unconfined"); + assert!(matches!(receipt, SubmitReceipt::Applied(_))); +} + /// 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. diff --git a/sidecar/claude-sdk/test/schemas.json b/sidecar/claude-sdk/test/schemas.json index 8a2fec6..08f189d 100644 --- a/sidecar/claude-sdk/test/schemas.json +++ b/sidecar/claude-sdk/test/schemas.json @@ -65,7 +65,7 @@ }, { "name": "declare_fact", - "parameters_json": "{\"additionalProperties\":false,\"properties\":{\"handle\":{\"description\":\"a new draft handle, unique within this patch\",\"type\":\"string\"},\"label\":{\"description\":\"a display label\",\"type\":\"string\"},\"standing\":{\"anyOf\":[{\"additionalProperties\":false,\"properties\":{\"evidence\":{\"description\":\"an exact evidence receipt\",\"type\":\"string\"},\"standing\":{\"const\":\"canonical\",\"type\":\"string\"}},\"required\":[\"standing\",\"evidence\"],\"type\":\"object\"},{\"additionalProperties\":false,\"properties\":{\"by\":{\"additionalProperties\":false,\"properties\":{\"ref\":{\"enum\":[\"draft\",\"existing\"],\"type\":\"string\"},\"value\":{\"description\":\"a draft handle declared in this same patch, or the canonical id of an existing subject\",\"type\":\"string\"}},\"required\":[\"ref\",\"value\"],\"type\":\"object\"},\"standing\":{\"const\":\"claimed\",\"type\":\"string\"}},\"required\":[\"standing\",\"by\"],\"type\":\"object\"}]},\"statement\":{\"description\":\"the committed world text this fact carries\",\"type\":\"string\"}},\"required\":[\"handle\",\"label\",\"statement\",\"standing\"],\"type\":\"object\"}" + "parameters_json": "{\"additionalProperties\":false,\"properties\":{\"handle\":{\"description\":\"a new draft handle, unique within this patch\",\"type\":\"string\"},\"label\":{\"description\":\"a display label\",\"type\":\"string\"},\"standing\":{\"anyOf\":[{\"additionalProperties\":false,\"properties\":{\"evidence\":{\"description\":\"an exact evidence receipt\",\"type\":\"string\"},\"standing\":{\"const\":\"canonical\",\"type\":\"string\"}},\"required\":[\"standing\",\"evidence\"],\"type\":\"object\"},{\"additionalProperties\":false,\"properties\":{\"by\":{\"additionalProperties\":false,\"properties\":{\"ref\":{\"enum\":[\"draft\",\"existing\"],\"type\":\"string\"},\"value\":{\"description\":\"a draft handle declared in this same patch, or the canonical id of an existing subject\",\"type\":\"string\"}},\"required\":[\"ref\",\"value\"],\"type\":\"object\"},\"standing\":{\"const\":\"claimed\",\"type\":\"string\"}},\"required\":[\"standing\",\"by\"],\"type\":\"object\"},{\"additionalProperties\":false,\"properties\":{\"standing\":{\"const\":\"ruled\",\"type\":\"string\"}},\"required\":[\"standing\"],\"type\":\"object\"}]},\"statement\":{\"description\":\"the committed world text this fact carries\",\"type\":\"string\"}},\"required\":[\"handle\",\"label\",\"statement\",\"standing\"],\"type\":\"object\"}" }, { "name": "declare_place", @@ -103,6 +103,10 @@ "name": "install_incumbent", "parameters_json": "{\"additionalProperties\":false,\"properties\":{\"incumbent\":{\"additionalProperties\":false,\"properties\":{\"ref\":{\"enum\":[\"draft\",\"existing\"],\"type\":\"string\"},\"value\":{\"description\":\"a draft handle declared in this same patch, or the canonical id of an existing subject\",\"type\":\"string\"}},\"required\":[\"ref\",\"value\"],\"type\":\"object\"},\"institution\":{\"additionalProperties\":false,\"properties\":{\"ref\":{\"enum\":[\"draft\",\"existing\"],\"type\":\"string\"},\"value\":{\"description\":\"a draft handle declared in this same patch, or the canonical id of an existing subject\",\"type\":\"string\"}},\"required\":[\"ref\",\"value\"],\"type\":\"object\"},\"office\":{\"description\":\"the office's name\",\"type\":\"string\"}},\"required\":[\"institution\",\"office\",\"incumbent\"],\"type\":\"object\"}" }, + { + "name": "mint", + "parameters_json": "{\"additionalProperties\":false,\"properties\":{\"holder\":{\"additionalProperties\":false,\"properties\":{\"ref\":{\"enum\":[\"draft\",\"existing\"],\"type\":\"string\"},\"value\":{\"description\":\"a draft handle declared in this same patch, or the canonical id of an existing subject\",\"type\":\"string\"}},\"required\":[\"ref\",\"value\"],\"type\":\"object\"},\"qty\":{\"maximum\":18446744073709551615,\"minimum\":1,\"type\":\"integer\"},\"resource\":{\"additionalProperties\":false,\"properties\":{\"ref\":{\"enum\":[\"draft\",\"existing\"],\"type\":\"string\"},\"value\":{\"description\":\"a draft handle declared in this same patch, or the canonical id of an existing resource\",\"type\":\"string\"}},\"required\":[\"ref\",\"value\"],\"type\":\"object\"}},\"required\":[\"holder\",\"resource\",\"qty\"],\"type\":\"object\"}" + }, { "name": "open_forum", "parameters_json": "{\"additionalProperties\":false,\"properties\":{\"forum\":{\"additionalProperties\":false,\"properties\":{\"ref\":{\"enum\":[\"draft\",\"existing\"],\"type\":\"string\"},\"value\":{\"description\":\"a draft handle declared in this same patch, or the canonical id of an existing subject\",\"type\":\"string\"}},\"required\":[\"ref\",\"value\"],\"type\":\"object\"},\"grievance\":{\"description\":\"the grievance kind\",\"type\":\"string\"},\"standing\":{\"anyOf\":[{\"additionalProperties\":false,\"properties\":{\"over\":{\"const\":\"subject\",\"type\":\"string\"},\"ref\":{\"additionalProperties\":false,\"properties\":{\"ref\":{\"enum\":[\"draft\",\"existing\"],\"type\":\"string\"},\"value\":{\"description\":\"a draft handle declared in this same patch, or the canonical id of an existing subject\",\"type\":\"string\"}},\"required\":[\"ref\",\"value\"],\"type\":\"object\"}},\"required\":[\"over\",\"ref\"],\"type\":\"object\"},{\"additionalProperties\":false,\"properties\":{\"over\":{\"const\":\"place_subtree\",\"type\":\"string\"},\"ref\":{\"additionalProperties\":false,\"properties\":{\"ref\":{\"enum\":[\"draft\",\"existing\"],\"type\":\"string\"},\"value\":{\"description\":\"a draft handle declared in this same patch, or the canonical id of an existing place\",\"type\":\"string\"}},\"required\":[\"ref\",\"value\"],\"type\":\"object\"}},\"required\":[\"over\",\"ref\"],\"type\":\"object\"}]}},\"required\":[\"grievance\",\"forum\",\"standing\"],\"type\":\"object\"}"