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: PermissionError

Raised when an actor attempts a governance transition they are not authorized to make.

class Governance(_approvers=<factory>, _grants=<factory>)[source]

Bases: object

Who may approve promotions into which scope – the org-governance ACL.

approvers is the live, queryable ACL; grants is 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: True in 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() raises GovernanceAuthorizationError for anyone else.

Parameters:
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 who is allowed to approve (or reject) promotion into scope.

Parameters:
Return type:

bool

grant(who, scope, *, by)[source]

Add who as an approver for scope (chainable) – audited, and gated once owned.

by must already be an approver of scope, unless scope currently has none at all (the bootstrap case: someone has to seed the first approver). Raises GovernanceAuthorizationError otherwise.

Parameters:
Return type:

Governance

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 with approve() or reject() first, rather than silently replacing it with a second one.

Parameters:
Return type:

list[str]

pending(substrate, *, to=None)[source]

Items awaiting approval (optionally only those proposed to scope to).

Parameters:
  • substrate (Substrate)

  • to (str | None)

Return type:

list[SubstrateItem]

approve(substrate, item_id, *, by, governance, to=None)[source]

Promote a pending item into its proposed scope – IFF by may 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 its proposal_history naming 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. Raises GovernanceAuthorizationError if by is not an approver authorized for the target scope; no mutation happens in that case.

Parameters:
  • substrate (Substrate)

  • item_id (str)

  • by (str)

  • governance (Governance)

  • to (str | None)

Return type:

bool

reject(substrate, item_id, *, by, governance, reason='')[source]

Refuse a pending promotion – the item stays in its origin scope; the refusal is recorded.

by must be either the proposal’s own proposer (a self-withdrawal) or an actor governance authorizes to approve into the proposed scope (a governance refusal). Anyone else raises GovernanceAuthorizationError – an unrelated third party can no longer reject someone else’s proposal. A new REJECTED entry is appended to the item’s proposal_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.

Parameters:
  • substrate (Substrate)

  • item_id (str)

  • by (str)

  • governance (Governance)

  • reason (str)

Return type:

bool

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.

Parameters:
  • substrate (Substrate)

  • item_id (str)

  • capability_id (str)

  • version (str)

  • digest (str | None)

Return type:

AuthorizationDecision