Add granting and revoking affordances to the world kernel (play agent Cut 5, PA-Q10)

ComponentOp::GrantAffordance{subject, affordance} and RevokeAffordance
resolve beside GrantAuthority/RevokeAuthority: a retired or externally
controlled subject, or an unknown catalog entry, is refused (the new
Mismatch::GrantsOutsideControl); a grant already held or a revoke not held is
NoOperationEffect; a revoke that would leave a controlled subject with no
affordance at all is refused with the new Mismatch::WouldLeaveSubjectMute,
because derive_opportunities requires at least one. Applying either writes
state.affordance_grants directly, re-deciding the same three refusals against
committed state. confine_to_ground now refuses both, unconditionally, beside
Retire: a grant is authority over the catalog, not structure over ground, so
the owner (including the seed lane, the owner in Draft) and the play
authority are the only callers.

PATCH_TOOLS gains grant_affordance and revoke_affordance (Operate, subject
plus an affordance reference); catalog counts move to (7, 34, 43). Same
consumer.v6 / controller_work.v17 as Cuts 3 and 4.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-22 04:54:35 +02:00
commit 732b1d33ad
3 changed files with 427 additions and 14 deletions

View file

@ -2995,6 +2995,37 @@ fn apply_operation(
state.positions.remove(subject);
state.affordance_grants.remove(&scope);
}
ResolvedOp::GrantAffordance { subject, affordance }
| ResolvedOp::RevokeAffordance { subject, affordance } => {
let granting = matches!(operation, ResolvedOp::GrantAffordance { .. });
let scope = DecisionScope {
subject_id: *subject,
};
let assignment = state.controller_assignments.get(&scope).ok_or_else(unknown)?;
if assignment.mode().is_none() || !state.affordance_catalog.contains_key(affordance) {
return Err(unknown());
}
let granted = state
.affordance_grants
.get_mut(&scope)
.ok_or_else(unknown)?;
let held = granted.contains(affordance);
if held == granting {
return Err(KernelError::Invariant(
"affordance grant operation changes nothing".into(),
));
}
if granting {
granted.insert(*affordance);
} else {
if granted.len() == 1 {
return Err(KernelError::Invariant(
"revoke would leave a controlled subject with no affordance".into(),
));
}
granted.remove(affordance);
}
}
}
Ok(())
}
@ -4625,12 +4656,17 @@ fn confine_to_ground(
}
for (index, operation) in resolved.operations.iter().enumerate() {
let site = Site::Operation(index);
// Retirement is authority over the subject's standing, not structure
// over ground: a confined author is refused whatever place the
// subject stands under, exactly as `require_ruler` refuses a `Ruled`
// fact or a `Mint` outright. The owner is unconfined and never
// reaches this clause.
if matches!(operation, ResolvedOp::Retire { .. }) {
// Retirement and an affordance grant are authority over the subject's
// standing, not structure over ground: a confined author is refused
// whatever place the subject stands under, exactly as `require_ruler`
// refuses a `Ruled` fact or a `Mint` outright. The owner is unconfined
// and never reaches this clause.
if matches!(
operation,
ResolvedOp::Retire { .. }
| ResolvedOp::GrantAffordance { .. }
| ResolvedOp::RevokeAffordance { .. }
) {
mismatches.push(Mismatch::OutsideJurisdiction { site: site.clone() });
continue;
}
@ -4851,12 +4887,14 @@ fn operation_ground(
}
(subjects, places(named), routes)
}
// Confinement never reaches this through the ordinary ground check:
// `confine_to_ground` refuses `Retire` unconditionally for every
// confined author before this function is called for it. The
// subject is named here so the match stays total and correct if
// that clause is ever bypassed.
ResolvedOp::Retire { subject } => (vec![*subject], Vec::new(), Vec::new()),
// Confinement never reaches these through the ordinary ground check:
// `confine_to_ground` refuses `Retire`, `GrantAffordance`, and
// `RevokeAffordance` unconditionally for every confined author before
// this function is called for them. The subject is named here so the
// match stays total and correct if that clause is ever bypassed.
ResolvedOp::Retire { subject }
| ResolvedOp::GrantAffordance { subject, .. }
| ResolvedOp::RevokeAffordance { subject, .. } => (vec![*subject], Vec::new(), Vec::new()),
}
}
@ -16734,4 +16772,244 @@ mod clock_tests {
.retired
);
}
// ---- Cut 5: granting and revoking affordances ---------------------
fn wave_affordance() -> Declaration {
Declaration::Affordance(AffordanceDeclaration {
handle: DraftHandle::new("wave"),
kind: AffordanceKindName("wave".into()),
roles: vec![RoleSpec {
role: Role("target".into()),
kind: RefKind::Subject(None),
}],
preconditions: Vec::new(),
effect_slots: vec![EffectSlot {
op_kind: ComponentOpKind::AdvancePressure {
by: PressureMagnitude(1),
},
roles: vec![Role("target".into())],
bounds: Bounds::None,
}],
outcome_bands: vec![OutcomeBand {
weight: 1,
effects: vec![0],
}],
carries_speech: false,
})
}
/// P5.1: the seed lane is the owner in Draft, and it grants the player a
/// verb the same way any owner-authored `GrantAffordance` does.
#[test]
fn the_seed_grants_the_player_a_verb_in_draft() {
let directory = tempfile::tempdir().unwrap();
let mut kernel = WorldKernel::create(
directory.path().join("world.cc"),
creation(CommandId::new(), "SeedGrantsDraft"),
&auth_principal(owner()),
)
.expect("a created world")
.0;
let before = kernel.snapshot().unwrap();
let commons = before.places[0].id;
let speak = super::tests::speak_entry(&kernel);
submit_owner(
&mut kernel,
&before,
CommandBody::AdmitPatch {
answers: None,
patch: WorldPatch {
declarations: vec![
wave_affordance(),
Declaration::Subject(SubjectDeclaration {
handle: DraftHandle::new("player"),
label: "The Player".into(),
kind: SubjectKind::Person,
controller: NewController::NarrativePersona,
affordances: BTreeSet::from([speak]),
position: Some(Ref::Existing(commons)),
}),
],
operations: vec![ComponentOp::GrantAffordance {
subject: Ref::Draft(DraftHandle::new("player")),
affordance: Ref::Draft(DraftHandle::new("wave")),
}],
evidence: Vec::new(),
},
},
);
let snapshot = kernel.snapshot().unwrap();
let player = snapshot
.subjects
.iter()
.find(|subject| subject.label == "The Player")
.expect("the declared player");
assert_eq!(player.affordances.len(), 2);
}
/// P5.1, and the scope digest half of Q10's design: a grant and a revoke
/// in Active both move the scope digest, so a stale opportunity is
/// `ScopeChanged` rather than silently honoured.
#[test]
fn the_play_authority_grants_and_revokes_in_active() {
let directory = tempfile::tempdir().unwrap();
let (mut kernel, clockwork, active) =
clock_kernel(directory.path(), "PlayGrantRevoke");
let stale = opportunity_for(&active, clockwork.reeve);
submit_as(
&mut kernel,
play_caller(),
CommandBody::AdmitPatch {
answers: None,
patch: WorldPatch {
declarations: vec![wave_affordance()],
operations: vec![ComponentOp::GrantAffordance {
subject: Ref::Existing(clockwork.reeve),
affordance: Ref::Draft(DraftHandle::new("wave")),
}],
evidence: Vec::new(),
},
},
)
.expect("the play authority grants a new verb");
let after_grant = kernel.snapshot().unwrap();
assert_eq!(
after_grant
.subjects
.iter()
.find(|subject| subject.id == clockwork.reeve)
.unwrap()
.affordances
.len(),
4
);
let error = submit_as(
&mut kernel,
CallerId::Controller(stale.controller_id),
CommandBody::DeclineDecision {
opportunity: stale.clone(),
},
)
.unwrap_err();
assert!(matches!(error, KernelError::ScopeChanged { .. }), "{error:?}");
submit_as(
&mut kernel,
play_caller(),
CommandBody::AdmitPatch {
answers: None,
patch: WorldPatch {
declarations: Vec::new(),
operations: vec![ComponentOp::RevokeAffordance {
subject: Ref::Existing(clockwork.reeve),
affordance: Ref::Existing(clockwork.threaten),
}],
evidence: Vec::new(),
},
},
)
.expect("the play authority revokes a held verb");
let after_revoke = kernel.snapshot().unwrap();
let reeve = after_revoke
.subjects
.iter()
.find(|subject| subject.id == clockwork.reeve)
.unwrap();
assert_eq!(reeve.affordances.len(), 3);
assert!(!reeve.affordances.contains(&clockwork.threaten));
}
/// P5.2: `derive_opportunities` requires at least one grant; a revoke
/// that would leave the reeve mute is refused before it ever reaches
/// that invariant.
#[test]
fn a_revoke_cannot_leave_a_subject_mute() {
let directory = tempfile::tempdir().unwrap();
let (mut kernel, clockwork, _) = clock_kernel(directory.path(), "RevokeMute");
let speak = super::tests::speak_entry(&kernel);
for affordance in [clockwork.deliver, clockwork.threaten] {
submit_as(
&mut kernel,
play_caller(),
CommandBody::AdmitPatch {
answers: None,
patch: WorldPatch {
declarations: Vec::new(),
operations: vec![ComponentOp::RevokeAffordance {
subject: Ref::Existing(clockwork.reeve),
affordance: Ref::Existing(affordance),
}],
evidence: Vec::new(),
},
},
)
.expect("the play authority revokes down toward one verb");
}
let Ref::Existing(speak_id) = speak else {
panic!("speak is a standing affordance");
};
let error = submit_as(
&mut kernel,
play_caller(),
CommandBody::AdmitPatch {
answers: None,
patch: WorldPatch {
declarations: Vec::new(),
operations: vec![ComponentOp::RevokeAffordance {
subject: Ref::Existing(clockwork.reeve),
affordance: Ref::Existing(speak_id),
}],
evidence: Vec::new(),
},
},
)
.unwrap_err();
assert!(
matches!(&error, KernelError::PatchRejected(set)
if set.contains(&Mismatch::WouldLeaveSubjectMute { operation: 0 })),
"{error:?}"
);
}
/// P5.1: `confine_to_ground` refuses `GrantAffordance` for a confined
/// author unconditionally, even for a subject this same patch declares
/// and places inside the elaborator's own jurisdiction.
#[test]
fn an_elaborator_cannot_grant() {
let directory = tempfile::tempdir().unwrap();
let (mut kernel, clockwork, _) = clock_kernel(directory.path(), "GrantElaborator");
let speak = super::tests::speak_entry(&kernel);
let answered = dead_end_boundary(&kernel);
let error = submit_as(
&mut kernel,
elaborator(JurisdictionKey::PlaceSubtree(clockwork.dead_end)),
CommandBody::AdmitPatch {
answers: Some(PatchAnswer::Boundary(answered)),
patch: WorldPatch {
declarations: vec![Declaration::Subject(SubjectDeclaration {
handle: DraftHandle::new("stray"),
label: "The Roadside Stray".into(),
kind: SubjectKind::Person,
controller: NewController::NarrativePersona,
affordances: BTreeSet::from([speak]),
position: Some(Ref::Existing(clockwork.dead_end)),
})],
operations: vec![ComponentOp::GrantAffordance {
subject: Ref::Draft(DraftHandle::new("stray")),
affordance: Ref::Existing(clockwork.threaten),
}],
evidence: Vec::new(),
},
},
)
.unwrap_err();
assert!(
matches!(&error, KernelError::PatchRejected(set)
if set.contains(&Mismatch::OutsideJurisdiction {
site: Site::Operation(0)
})),
"{error:?}"
);
}
}

View file

@ -1207,6 +1207,20 @@ pub(crate) enum ComponentOp {
Retire {
subject: Ref<SubjectId>,
},
/// Grants one more standing catalog entry to an existing subject. Only the
/// owner (including the seed lane, the owner in Draft) and `Play` may
/// write it: a grant is authority, not structure, so a confined author is
/// refused whatever ground the subject stands on.
GrantAffordance {
subject: Ref<SubjectId>,
affordance: Ref<AffordanceId>,
},
/// Revokes a grant. Refused if it would leave a controlled subject with
/// none: `derive_opportunities` requires at least one.
RevokeAffordance {
subject: Ref<SubjectId>,
affordance: Ref<AffordanceId>,
},
}
/// What an Active `AdmitPatch` answers. Draft answers nothing; Active must
@ -1586,6 +1600,18 @@ pub enum Mismatch {
RetiresAMirror {
operation: usize,
},
/// `GrantAffordance` or `RevokeAffordance` named a retired or an
/// `ExternallyControlled` subject: neither holds a grant this patch may
/// touch.
GrantsOutsideControl {
operation: usize,
},
/// A `RevokeAffordance` that would leave a controlled subject with no
/// affordance at all. `derive_opportunities` requires at least one; a
/// subject is never left mute.
WouldLeaveSubjectMute {
operation: usize,
},
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
@ -1883,6 +1909,14 @@ pub(crate) enum ResolvedOp {
Retire {
subject: SubjectId,
},
GrantAffordance {
subject: SubjectId,
affordance: AffordanceId,
},
RevokeAffordance {
subject: SubjectId,
affordance: AffordanceId,
},
}
/// One resource's movement across a whole patch. Accumulators are `u128` so an
@ -4209,6 +4243,61 @@ pub(super) fn resolve_patch(
}
}
}
ComponentOp::GrantAffordance { subject, affordance }
| ComponentOp::RevokeAffordance { subject, affordance } => {
let granting = matches!(operation, ComponentOp::GrantAffordance { .. });
let subject_key = resolve_subject(
Site::Operation(position),
subject,
&index,
&state.subjects,
&mut mismatches,
);
let affordance_key = resolve_affordance(
Site::Operation(position),
affordance,
&index,
&state.affordance_catalog,
&mut mismatches,
);
let (Some(subject_key), Some(affordance_key)) = (subject_key, affordance_key)
else {
continue;
};
// A grant is authority over the catalog, not structure over
// ground: retirement and a mirror both hold no grant a patch
// may touch, whatever confinement later decides.
if retired.contains(&subject_key) || mirrors.contains(&subject_key) {
mismatches.push(Mismatch::GrantsOutsideControl {
operation: position,
});
continue;
}
let held = affordance_holdings
.get(&subject_key)
.is_some_and(|granted| granted.contains(&affordance_key));
if held == granting {
mismatches.push(Mismatch::NoOperationEffect {
operation: position,
});
continue;
}
if granting {
affordance_holdings
.entry(subject_key)
.or_default()
.insert(affordance_key);
} else {
let granted = affordance_holdings.entry(subject_key).or_default();
if granted.len() == 1 {
mismatches.push(Mismatch::WouldLeaveSubjectMute {
operation: position,
});
continue;
}
granted.remove(&affordance_key);
}
}
ComponentOp::OpenOffice {
institution,
office,
@ -5239,6 +5328,20 @@ pub(super) fn resolve_patch(
ComponentOp::Retire { subject } => ResolvedOp::Retire {
subject: subject_id_of(&key_of(subject)),
},
ComponentOp::GrantAffordance {
subject,
affordance,
} => ResolvedOp::GrantAffordance {
subject: subject_id_of(&key_of(subject)),
affordance: affordance_id_of(&key_of(affordance)),
},
ComponentOp::RevokeAffordance {
subject,
affordance,
} => ResolvedOp::RevokeAffordance {
subject: subject_id_of(&key_of(subject)),
affordance: affordance_id_of(&key_of(affordance)),
},
ComponentOp::Relocate { subject, via } => ResolvedOp::Relocate {
subject_id: subject_id_of(&key_of(subject)),
edge_id: edge_id_of(&key_of(via)),
@ -6282,6 +6385,28 @@ pub(crate) const PATCH_TOOLS: &[PatchTool] = &[
fields: &[field("subject", PatchFieldKind::Reference("subject"))],
shape: PatchToolShape::Operate { variant: "retire" },
},
PatchTool {
name: "grant_affordance",
description: "Grant a subject one more affordance from the standing catalog. Only the owner and the play table may grant.",
fields: &[
field("subject", PatchFieldKind::Reference("subject")),
field("affordance", PatchFieldKind::Reference("affordance")),
],
shape: PatchToolShape::Operate {
variant: "grant_affordance",
},
},
PatchTool {
name: "revoke_affordance",
description: "Revoke one of a subject's affordances. Refused if it would leave the subject with none. Only the owner and the play table may revoke.",
fields: &[
field("subject", PatchFieldKind::Reference("subject")),
field("affordance", PatchFieldKind::Reference("affordance")),
],
shape: PatchToolShape::Operate {
variant: "revoke_affordance",
},
},
PatchTool {
name: RECORD_GAP_PATCH_TOOL,
description: "Record something the world needs that this vocabulary cannot say. It changes nothing.",
@ -7023,7 +7148,7 @@ mod catalog_tests {
.iter()
.filter(|entry| matches!(entry.shape, PatchToolShape::Operate { .. }))
.count();
assert_eq!((declarations, operations, PATCH_TOOLS.len()), (7, 32, 41));
assert_eq!((declarations, operations, PATCH_TOOLS.len()), (7, 34, 43));
// Every declaration variant the vocabulary owns is reachable, and the
// two payload-carrying entity kinds are not exposed as an `Entity`
@ -7119,6 +7244,8 @@ mod catalog_tests {
ComponentOp::ReducePressure { .. } => "reduce_pressure",
ComponentOp::ResolvePressure { .. } => "resolve_pressure",
ComponentOp::Retire { .. } => "retire",
ComponentOp::GrantAffordance { .. } => "grant_affordance",
ComponentOp::RevokeAffordance { .. } => "revoke_affordance",
}
}
PATCH_TOOLS
@ -7170,7 +7297,7 @@ mod catalog_tests {
.count();
assert_eq!(
(declare_tools, operate_tools, PATCH_TOOLS.len()),
(7, 32, 41)
(7, 34, 43)
);
let declarations = every_declaration();

View file

@ -95,6 +95,10 @@
"name": "forget",
"parameters_json": "{\"additionalProperties\":false,\"properties\":{\"fact\":{\"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 fact\",\"type\":\"string\"}},\"required\":[\"ref\",\"value\"],\"type\":\"object\"},\"subject\":{\"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\":[\"subject\",\"fact\"],\"type\":\"object\"}"
},
{
"name": "grant_affordance",
"parameters_json": "{\"additionalProperties\":false,\"properties\":{\"affordance\":{\"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 affordance\",\"type\":\"string\"}},\"required\":[\"ref\",\"value\"],\"type\":\"object\"},\"subject\":{\"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\":[\"subject\",\"affordance\"],\"type\":\"object\"}"
},
{
"name": "grant_authority",
"parameters_json": "{\"additionalProperties\":false,\"properties\":{\"grant\":{\"additionalProperties\":false,\"properties\":{\"kind\":{\"description\":\"the authority kind\",\"type\":\"string\"},\"over\":{\"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\":[\"kind\",\"over\"],\"type\":\"object\"},\"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\"}},\"required\":[\"holder\",\"grant\"],\"type\":\"object\"}"
@ -147,6 +151,10 @@
"name": "retire",
"parameters_json": "{\"additionalProperties\":false,\"properties\":{\"subject\":{\"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\":[\"subject\"],\"type\":\"object\"}"
},
{
"name": "revoke_affordance",
"parameters_json": "{\"additionalProperties\":false,\"properties\":{\"affordance\":{\"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 affordance\",\"type\":\"string\"}},\"required\":[\"ref\",\"value\"],\"type\":\"object\"},\"subject\":{\"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\":[\"subject\",\"affordance\"],\"type\":\"object\"}"
},
{
"name": "revoke_authority",
"parameters_json": "{\"additionalProperties\":false,\"properties\":{\"grant\":{\"additionalProperties\":false,\"properties\":{\"kind\":{\"description\":\"the authority kind\",\"type\":\"string\"},\"over\":{\"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\":[\"kind\",\"over\"],\"type\":\"object\"},\"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\"}},\"required\":[\"holder\",\"grant\"],\"type\":\"object\"}"