mixle.substrate.governance module¶
Promotion gates for shared substrate knowledge.
Teams can publish items into shared scopes, while curated scopes can require an
explicit approval step. propose() marks an item as pending promotion,
approve() promotes it when the approver is authorized for the target
scope, reject() refuses it, and pending() lists items awaiting
review.
Every transition names an explicit actor (by=) – there is no implicit or
ambient identity – and is checked against Governance’s approver ACL.
approve() requires by to be an authorized approver of the target
scope; reject() requires by to be either the proposal’s own proposer
(a self-withdrawal) or an authorized approver (a governance refusal); an actor
who is neither raises GovernanceAuthorizationError rather than
silently succeeding or no-op’ing. Governance.grant() is gated the same
way: only an existing approver of a scope may grant further approvers into it,
except to bootstrap a scope that has none yet.
Decisions are append-only. The full history of an item’s proposals lives in
item.provenance["proposal_history"]; a transition is always recorded as a
NEW entry referencing (supersedes) the entry it decides, never as an
in-place overwrite, so a rejected or approved decision can never be silently
erased by a later propose() call. item.provenance["proposal"] always
mirrors the latest entry, for callers that only care about current state.
Approved promotion delegates the scope change to
publish(), preserving the same provenance trail as
ordinary sharing.
- exception GovernanceAuthorizationError[source]
Bases:
PermissionErrorRaised when an actor attempts a governance transition they are not authorized to make.
- class Governance(_approvers=<factory>, _grants=<factory>)[source]
Bases:
objectWho may approve promotions into which scope – the org-governance ACL.
approversis the live, queryable ACL;grantsis its append-only audit trail (who was granted, into which scope, and who authorized it). Both are READ-ONLY views: they were public mutable containers, so authority could be granted straight into the dict with no authorization check and no audit entry, and the trail recording that could itself be edited (MXR-080-1883).grant()is the only way in, and it both checks and records.A scope with no approvers yet is unowned: the first grant into it may come from anyone, so an approver set can be bootstrapped from nothing. That remains a genuine hole – anyone may claim an unowned scope – and it is now marked
bootstrap: Truein the audit trail rather than being indistinguishable from an authorized grant. Rooting the bootstrap in a declared authority is outstanding. Once a scope has at least one approver, only an existing approver of THAT scope may grant further ones into it –grant()raisesGovernanceAuthorizationErrorfor anyone else.- property approvers: Mapping[str, frozenset[str]]
The live ACL, as a read-only view.
This was a public mutable dict, so
governance.approvers.setdefault("secret", set()).add(...)granted approval authority over a scope with no authorization check and no entry in the audit trail that exists to record exactly that (MXR-080-1883).grant()is the only way in, and it both checks and records.
- property grants: tuple[Mapping[str, Any], ...]
The append-only grant audit trail, as a read-only view.
An audit trail a caller can edit or truncate is not one; it was a public
list.
- may_approve(who, scope)[source]
Return whether
whois allowed to approve (or reject) promotion intoscope.
- grant(who, scope, *, by)[source]
Add
whoas an approver forscope(chainable) – audited, and gated once owned.bymust already be an approver ofscope, unlessscopecurrently has none at all (the bootstrap case: someone has to seed the first approver). RaisesGovernanceAuthorizationErrorotherwise.
- propose(substrate, ids, *, to, by)[source]
Mark items as pending promotion to scope
to; they are not yet visible there. Returns the ids.Each call appends a NEW entry to the item’s append-only
proposal_history– it never overwrites an existing entry, so a prior decision’s audit record (who rejected or approved it, and why) always survives, even when the item is proposed again later. An item that already has a PENDING proposal is skipped (left out of the returned ids): resolve that in-flight review withapprove()orreject()first, rather than silently replacing it with a second one.
- pending(substrate, *, to=None)[source]
Items awaiting approval (optionally only those proposed to scope
to).
- approve(substrate, item_id, *, by, governance, to=None)[source]
Promote a pending item into its proposed scope – IFF
bymay approve for that scope (the gate).On success the item is published into the target scope (via P1
publish(), so it inherits the versioned/audited share) and a new APPROVED entry is appended to itsproposal_historynaming the approver – the PENDING entry it decides is left untouched, never overwritten. Returns False (no change, not a security event) if the item has no pending proposal. RaisesGovernanceAuthorizationErrorifbyis not an approver authorized for the target scope; no mutation happens in that case.
- reject(substrate, item_id, *, by, governance, reason='')[source]
Refuse a pending promotion – the item stays in its origin scope; the refusal is recorded.
bymust be either the proposal’s own proposer (a self-withdrawal) or an actorgovernanceauthorizes to approve into the proposed scope (a governance refusal). Anyone else raisesGovernanceAuthorizationError– an unrelated third party can no longer reject someone else’s proposal. A new REJECTED entry is appended to the item’sproposal_history; the PENDING entry it decides is left untouched, never overwritten. Returns False (no change, not a security event) if the item has no pending proposal to refuse.
- authorization_decision(substrate, item_id, *, capability_id, version, digest=None)[source]
Adapt a completed legacy scope proposal to the shared authorization contract.
Pending or missing proposals have no decision and raise
ValueError. This adapter keeps the substrate API compatible while allowing orchestrators to exchange one authorization representation with other Mixle projects.