Actors, ODRL and PROV-O: one permission language, one execution log
What the owner decided, 2026-09-23
“Actors can be instantiated…. actor schema instancnes could then be referenced. tools is what that corresponds to (e.g. github user). whatever the tool is, the process is that the data store manage authoentication and authorixation of the KG DS. up to their permission Actors should be able to visualize, render, commment on and or manipulate the KG or otherwise engage in a named Task under a Process with a specific Role.”
Then, choosing among options:
- Identity: the login → Actor mapping lives in the data store only. An actor file never carries a login.
- Grant scope: by Process, Task or Role, and no scope means everywhere.
- Permissions: “want more granular permissions, can inherit. options for existing json open standards. doensnt necc need to be rooted acyclic graph.” Then, having seen the comparison: W3C ODRL 2.2, with W3C PROV-O for logging.
- Where this is going: “need to get to computable data sharing agreements.
something that can work w/ digital wallets, gdhcn+vc’s+hcert”. ODRL’s
Agreementis that artefact, and this proposal keeps the door open to it without building it (§6).
1. What is there today, and why it is too much
An actor file (.claude/skills/actors/*.json, 32 of them) carries three
lists that answer three questions:
| field | answers | e.g. admin.json |
|---|---|---|
roles | what it may act as | administrator, editor, reviewer, … |
permissions | what it may do, in any lane | admin-settings, role-management, release-authorization |
capabilities | what its machine has | git-push |
permissions draws on 14 flat ids in skills/permissions/permissions.json.
They have no hierarchy, no scope and no standard behind them, and nothing
enforces them (bean bkje). Identity exists only as prose in
skills/sdlc/sdlc-core/deployment-auth.md, as a gateway role (viewer,
collaborator, owner) that joins to nothing here.
2. The simplified model
Four things, each borrowed from a standard, and nothing invented that a standard already names.
2.1 Actor = prov:Agent, and nothing about permission
// .claude/skills/actors/admin.json — after
{
"id": "admin",
"@type": "prov:Person", // person → prov:Person; agent, system → prov:SoftwareAgent; external → prov:Organization
"title": "Administrator",
"roles": ["administrator", "programme-manager", "editor", "reviewer"],
"capabilities": ["git-push"],
"identities": [{ "tool": "github-user", "withheld": true }]
}
permissionsleaves the actor file. What an actor may do becomes an ODRL rule about the actor (§2.3), so it lives in one place for every actor.kindbecomes the PROV-O class. The fourACTOR_KINDSmap onto PROV-O’s three agent classes, and nothing is lost.rolesstays. It is eligibility, “who is expected in a lane”, which is whatrole-model.mdalready says it is. It is not a permission.capabilitiesstays. It is a fact about the machine (can it push, is it air-gapped), not a grant.actor-facts-and-their-processesis about those facts.identitiesis new and says only that a binding exists. The login is in the data store.withheld: trueis the default, and a published actor file carrying a login is a QA failure.
2.2 Permission ids → an ODRL profile of actions
The 14 ids become actions in a folio ODRL profile, and each one is
odrl:includedIn an action from ODRL’s own vocabulary. That is the inheritance
the owner asked for. includedIn forms a graph with no required root, and one
action may sit under several.
| folio action | odrl:includedIn | was |
|---|---|---|
cat-harness:visualize | odrl:display | new |
cat-harness:render | odrl:derive | new |
cat-harness:comment | odrl:annotate | review-comments |
cat-harness:author | odrl:modify | content-authoring |
cat-harness:translate | odrl:translate | translation |
cat-harness:administer | odrl:modify | admin-settings, role-management, told apart by target (the settings, the role graph) |
cat-harness:perform | odrl:execute | new: perform a named Task |
cat-harness:approve, cat-harness:adjudicate, cat-harness:first-pass-review, cat-harness:coordinate-smes, cat-harness:validate-clinically, cat-harness:report-qa, cat-harness:govern, cat-harness:authorize-release, cat-harness:manage-release | cat-harness:perform | the other 9 ids, which are all kinds of task |
The last row is the simplification. Nine of today’s 14 ids are not really
permissions to touch the graph. They are permission to perform a particular
kind of task, so they sit under cat-harness:perform and are then scoped by
constraints (§2.3) instead of each becoming its own global switch. Two more
were one action on two targets, and ODRL’s target says which.
2.3 Grants → ODRL rules, scoped by Process, Task and Role
// policies/folio-defaults.jsonld — one odrl:Set for the instance
{
"@context": ["http://www.w3.org/ns/odrl.jsonld", { "folio": "…/ns/folio#" }],
"@type": "Set",
"uid": "…/policies/folio-defaults",
"profile": "…/ns/folio-odrl",
"permission": [
{ "assignee": "actor:admin", "action": "cat-harness:authorize-release",
"constraint": [{ "leftOperand": "cat-harness:process", "operator": "eq",
"rightOperand": "release" }] },
{ "assignee": "actor:admin", "action": "cat-harness:administer", "target": "graph:roles" },
{ "assignee": "actor:reviewer-agent", "action": "cat-harness:comment" },
{ "assignee": "cat-harness:anyone", "action": "cat-harness:visualize" }
]
}
- Scope is a constraint. The profile defines three
leftOperands:cat-harness:process,cat-harness:taskandcat-harness:role. No constraint means everywhere, which is the owner’s rule. - Performing a task needs two things: a permission for
cat-harness:performwhose constraints match the task, and the actor holding the lane’s role. Role eligibility (roles) and permission stay separate, asrole-model.mdalready insists. - Inheritance between policies is
odrl:inheritFrom. An instance’s policy inherits cat-harness’s defaults, the same wayneedsinherits everything else. - Conflict is ODRL’s own
conflictterm (perm,prohibitorinvalid), so “what if a permission and a prohibition both match” has a standard answer instead of ours.
2.4 Every task run → a prov:Activity
{
"@type": "prov:Activity",
"prov:startedAtTime": "2026-09-23T19:36:08Z",
"prov:qualifiedAssociation": {
"prov:agent": "actor:authoring-agent",
"prov:hadRole": "role:author",
"prov:hadPlan": "process:content-lifecycle#Task_author" // the BPMN task is the prov:Plan
},
"prov:actedOnBehalfOf": "actor:admin", // an agent working for a person
"prov:used": ["kg:…"], "prov:generated": ["kg:…"],
"cat-harness:underPolicy": "…/policies/folio-defaults" // the one non-PROV term
}
PROV-O already has every piece the log needs:
hadPlanis the BPMN task;hadRoleis the lane’s role;actedOnBehalfOfis a swarm agent working for a person;usedandgeneratedare the graph nodes touched.
3. Why this fits both BPMN engines
See BPMN execution: one skill, two engines.
- The deterministic engine reads the ODRL policy before each task and
refuses one that no permission covers. It writes the
prov:Activityas it runs. Built 2026-09-23, advisory (issue #1207): the engine refuses adenyor a role mismatch and recordsunknown, because no policy yet grantsperform-taskfor any lane. See thetask-authorizationskill. The PROV activity is not written yet. - The agentic swarm acts first. Its QA/QC report reads the PROV-O log
after, and every
prov:Activitywith no matching ODRL permission, or ahadRolethe lane does not bind, is a finding. Built 2026-09-24, advisory (#1180 step 5):scripts/prov-qaqc.tsderives the PROV-O log from each workflow instance’s history, writes it todocs/assets/prov/<instance>.prov.jsonld, and re-runsauthorizeTaskon every step with the actorasserted. Findings are listed on the/prov-qaqc/page and do not fail the build;check:prov-qaqcfails only on stale outputs or a record that does not validate. An entry with no actor or no lane role gets a finding, not aprov:Activity, because the schema requires both. See thetask-authorizationskill.
It is the same policy and the same log, checked at two different times. That is exactly how the figure describes the mitigation at the agentic end: “mechanical + agentic QA/QC reports”.
4. What gets deleted
skills/permissions/permissions.jsonbecomes the ODRL profile. All 14 ids survive as actions or action + target, with their descriptions.permissionsin every actor file moves intopolicies/*.jsonld.- The gateway roles (viewer, collaborator, owner) in
deployment-auth.mdbecome default policies. The data store maps a login to an actor and evaluates ODRL. LoadedActor.permissionsinschemas/role-graph.tsgives way to a purepermits(actor, action, scope, policies)that returnspermit,denyorunknown.unknownis never permit.
What does not change: RoleDef, lanes and role inherits, RACI (which is
responsibility, not permission, as raci.md says), and capabilities.
5. What this needs, and in what order
Each step is its own PR with its own QA, and missing QA is a failure:
- The profile and the schemas:
schemas/odrl.ts: zod for the ODRL subset used (Set, Agreement, Rule, Constraint, Party), validated against the W3C JSON-LD context;schemas/prov.ts: the PROV-O Activity/Association subset;- the profile as JSON-LD, with a test that every
includedInresolves.
- Migration: a script moves
permissionsout of the 32 actor files intopolicies/folio-defaults.jsonld. A test checks that no actor lost or gained a permission, by comparing before and afterpermits. permits(): a table-driven test covering permit, deny, unknown, the role check and the conflict strategy.- The skills, rewritten:
role-model.md: three lists, and permissions are policies;deployment-auth.md;process-state.md: entering a task names its permission;bpmn-authoring.md: lanes bind roles, never people;raci.md.
- The QA/QC report over PROV-O: the agentic end’s mitigation, which is the
first real consumer of all this.
Built 2026-09-24:
scripts/prov-qaqc.ts(bun run cat prov:qaqc, gated bycheck:prov-qaqc), tested inscripts/tests/prov-qaqc.test.ts. It reusesauthorizeTask,laneBindingandProvActivitySchemarather than restating them.cat-harness:underPolicynow takes an array, becausedecideevaluates every policy in force and naming one would misstate the verdict. First run, over the 4 committed instances: 55 steps, 55 activities, and every stepunknown(no policy grantsperform-task) with an undeclared actor (claude,litlfred). The history records who, not which declared actor, so the log is only as good as the name written into it. 2026-09-24, owner: “Declare both”.claudeis now declared (an agent, rolesauthoring-agentandcode-reviewer).litlfredis not: it is a GitHub login, and an actor file never carries one. Beanjwocrecords why and what the owner can choose;prov:hadRolestays required. - ihris:
- the 2009 use-case actors (A-PT1 HR Manager, …) become Roles;
- “Assigned To” and “Source” become opaque
prov:Personactors (ihris-2009-staff-NN), one each, with the identity withheld; - a leak check fails if a withheld name reaches
library/or the site.
6. Not in this proposal: data sharing agreements
A computable data sharing agreement is an odrl:Agreement: an assigner, an
assignee, and permissions, prohibitions and duties. Nothing above prevents
signing one as a W3C Verifiable Credential, holding it in a wallet, trusting
its issuer through the WHO GDHCN trust lists (did:web:tng-cdn.who.int:v2:trustlist:-:<country>,
already indexed by the trust IG), or carrying it as an HCERT. That is the next
proposal, and it depends on this one: an Agreement is only as computable as the
actions and constraints it is written in.
7. Open questions for the owner
- Where policies live.
policies/at the instance root, or a declared directory with its own graph typology? A graph typology is recommended, because it is how everything else gets a viewer and QA. - Default for an unauthenticated reader.
cat-harness:visualizeandcat-harness:renderonly, or nothing? - Enforcement engine. Evaluate ODRL directly in the data store, or compile it to a relationship engine (OpenFGA/Zanzibar) there? The model above is the same either way, so this can wait until a data store is built.
What would change this proposal
- If an ODRL constraint cannot express a scope we need (say, “only while the
bean is in state X”), the fix is a new
leftOperandin the profile. That would only change the proposal if it needed a rule shape ODRL does not have. - If PROV-O cannot record something the QA/QC report must check, that goes
in as a
folio:term beside the PROV-O ones, the wayunderPolicydoes above.