Add the play authority, Ruled facts and Mint (kernel), schema consumer.v6 (play agent Cut 3, PA-Q5/PA-Q15)

SystemCapability::Play is a new system capability, minted only by the new
PlayPort (mailbox.rs): unconfined, declares in Active with no answer, may
advance time, and is the sole author of FactStanding::Ruled and
ComponentOp::Mint (quantity with no evidence receipt). One new admission rule,
require_ruler, refuses a Ruled fact or a Mint from any other caller in both
reduce and apply_effect; the evidence gate on Canonical facts and Admit is
untouched for every author, Play included. controller_work rows bump to v17
and the consumer schema bumps to consumer.v6 in the same commit, since the
shape change and the version are one bump.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-22 02:58:03 +02:00
commit 4c4973b7e8
8 changed files with 993 additions and 19 deletions

View file

@ -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("<redacted>"), "{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);
}
}
}

View file

@ -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

View file

@ -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

View file

@ -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
/// `<family>-<version>` 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<WorldEffect,
// a jurisdiction complaint about a reference that does not resolve.
require_answer(
state,
&command.caller,
answers.as_ref(),
!(patch.declarations.is_empty() && patch.evidence.is_empty()),
)?;
let confinement = require_patch_author(state, &command.caller, answers.as_ref())?;
let resolved = patch::resolve_patch(state, command.id, patch, None, None)
.map_err(KernelError::PatchRejected)?;
require_ruler(&command.caller, &resolved).map_err(KernelError::PatchRejected)?;
if let Some(ground) = confinement {
confine_to_ground(state, &resolved, ground).map_err(KernelError::PatchRejected)?;
}
@ -1709,8 +1715,11 @@ fn require_system_capability(caller: &CallerId, body: &CommandBody) -> 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<EntityId> {
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<WorldSnapshot, KernelError> {
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<Option<PatchGround>, 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<Mismatch>> {
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);
}
}

View file

@ -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<WorldSnapshot, MailboxError> {
self.mailbox.snapshot().await
}
pub async fn submit_patch(
&self,
command_id: CommandId,
patch: WorldPatch,
) -> Result<SubmitReceipt, MailboxError> {
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<SubmitReceipt, MailboxError> {
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

View file

@ -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<SubjectId> },
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<SubjectId>,
resource: Ref<EntityId>,
qty: Quantity,
},
Bind {
subject: Ref<SubjectId>,
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<SubjectId>),
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();

View file

@ -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.

View file

@ -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\"}"