mixle.substrate.spaces module¶
Team-scoped substrate views with explicit publishing, centralized access control, and immutable history.
The substrate tags every item with an access scope (“local”, a team id, or a shared scope like
“public”), but the raw store filters on one scope at a time. A team’s real visibility is a union: its
own items plus whatever has been shared into a common scope, and never another team’s private items.
Space is that view. Construct one for a team and it answers retrieve / all over exactly
the team’s visible set, so two teams querying the same substrate see different, correctly-isolated
knowledge.
Sharing is explicit: nothing crosses a scope boundary until someone calls
publish(). Publishing re-scopes an item into a shared scope and records
who published it and from where.
Isolation is enforced, not caller convention (MXR-080-0264). Every read, write, and publish is checked
against an AccessPolicy – the single, centralized decision of whether a principal may touch a
scope. A principal always has full access to its own home scope (the scope sharing its name) and to
PUBLIC, the explicit sharing commons; every OTHER scope, in particular another team’s private
scope, is denied unless AccessPolicy.grant() says otherwise. Space validates shared
against the policy at construction (and again, live, on every read, so a later revocation takes effect
immediately); publish() checks both the item’s actual stored scope and the target; and
merge_versions() requires the merging principal to hold write access to both sides. None of these
trust a caller-supplied scope parameter as if it were already a validated permission.
Every publish and merge also persists an immutable, content-addressed snapshot of the state it is about
to overwrite or delete (MXR-080-0265/0266). version_history entries reference those snapshots by
digest; revision() recovers the complete prior item – text, payload, provenance, tags, links,
scope, timestamp – from one, so “the prior state is recoverable” is an actual, checkable property, not
just an audit label. merge_versions() additionally requires a typed MergeStrategy and
authorized common lineage, and only performs its destructive step (deleting the merged-away item, whose
full state remains recoverable via its snapshot) when the caller explicitly passes confirm=True.
- exception AccessDeniedError[source]
Bases:
PermissionErrorRaised when a principal is not authorized for the scope it tried to read, write, or publish.
- class AccessPolicy(_read=<factory>, _write=<factory>)[source]
Bases:
objectThe centralized access-policy decision point (MXR-080-0264): the one place that decides whether a principal may read or write a scope.
A principal always has read+write access to the scope sharing its own name (
Spaceis constructed withteamdoubling as that team’s principal id, so a team’s own scope is always its home scope) and toPUBLIC, the explicit sharing commons – this module’s whole design is that nothing else crosses a boundary until someone deliberately publishes into a common scope. Every OTHER scope, in particular another team’s private scope, is denied until explicitly granted withgrant_read()/grant_write()/grant().Space,publish(), andmerge_versions()all consult anAccessPolicyinstead of trusting caller-supplied scope parameters (shared=,to=,from_scope=) as though they were already-validated permissions – closing the concrete leak where a team-A space configured withshared=("team-b",)simply read team-B’s private items with no check at all.- grant_read(principal, scope)[source]
Grant
principalread access toscopebeyond its home scope and PUBLIC (chainable).
- grant_write(principal, scope)[source]
Grant
principalwrite access toscopebeyond its home scope and PUBLIC (chainable).
- grant(principal, scope)[source]
Grant
principalboth read and write access toscope(chainable).
- revoke(principal, scope)[source]
Revoke both read and write grants of
scopefromprincipal(chainable).A principal’s home scope and PUBLIC are intrinsic, not grant-dependent, so this can never revoke those – only a scope previously given via
grant_read()/grant_write().
- can_read(principal, scope)[source]
Whether
principalmay read items inscope.
- can_write(principal, scope)[source]
Whether
principalmay write (add or publish) items intoscope.
- require_read(principal, scope)[source]
Raise
AccessDeniedErrorunlessprincipalmay readscope.
- visible_scopes(team, *, shared=(PUBLIC,))[source]
The scopes a team may read: its own id plus the shared scopes (never another team’s private one).
A pure helper – it does not itself enforce anything.
Spaceis the enforcement point: it validatessharedagainst anAccessPolicybefore this ever runs.
- revision(substrate, digest)[source]
The complete immutable snapshot recorded under content hash
digest(MXR-080-0265), orNoneif no such revision was ever persisted. Returns the full priorSubstrateItemstate as a JSON-compatible dict (kind, text, payload, provenance, tags, links, scope, created_at) – not just the who/from/to bookkeepingversion_historyalone would give you. The returned dict is independent of the stored snapshot (a deep copy), so a caller mutating it can never corrupt the persisted revision.
- publish(substrate, ids, *, to=PUBLIC, by, from_scope=None, policy=None)[source]
Share items into a common scope, authorized (MXR-080-0264).
Re-scopes each item in
idstotoand recordspublished_by/published_fromin its provenance. Returns the ids actually published; missing ids, ids whose scope doesn’t matchfrom_scope(an additional, OPTIONAL caller assertion), and idsbyis not authorized to move are all skipped rather than aborting the whole batch.bymust be a non-empty, authenticated principal, andpolicy(anAccessPolicy; a fresh, all-deny-but-home-scope-and-PUBLIC one when omitted) must authorize it to WRITEto– checked once, up front, for the whole call – AND to READ each item’s ACTUAL stored scope, checked per item. That second check is what closes the holefrom_scopealone never did: it fires even whenfrom_scopeis omitted entirely, so a known item id from a scopebycannot read is never movable just by leaving the filter off.Every published item’s pre-publish state is persisted as an immutable, content-addressed revision (MXR-080-0265) before it is overwritten;
version_historyentries carry that revision’s digest, recoverable withrevision()– so “a re-published item never silently overwrites its predecessor, the prior state is always recoverable” is an enforced property, not just a claim.
- version_of(item)[source]
The share version of an item (0 if never published) – a monotonic counter bumped by each publish.
- history(substrate, item_id)[source]
The full publish/merge history of an item: every version with who changed it, from where, and to where, plus (MXR-080-0265/0266) a
"revision"(publish entries) orparents[]["revision"](merge entries) content-hash digest thatrevision()resolves to the complete immutable prior item state – not just this bookkeeping.
- class MergeStrategy(*values)[source]
Bases:
StrEnumWhich side’s text/payload survives a merge – a closed, validated vocabulary (MXR-080-0266): an unrecognized strategy now raises instead of silently behaving like
KEEP.
- merge_versions(substrate, keep_id, other_id, *, by=None, prefer=MergeStrategy.LATEST, policy=None, confirm=False)[source]
Reconcile two versions of the same knowledge into one, keeping full lineage (no silent loss, P2).
Merges
other_idintokeep_id: unions tags and links, keeps the text/payload of whicheverprefer(aMergeStrategy) selects, bumps the surviving item’s version, records BOTH parents in the history, and removes the merged-away item. Returns the surviving id, orNoneif either id is missing (checked first, before any authorization – a merge naming a nonexistent id is a no-op, not a permission question).Authorized common lineage, required (MXR-080-0266):
bymust be a non-empty, authenticated principal, andpolicy(anAccessPolicy; a fresh, all-deny-but-home-scope-and-PUBLIC one when omitted) must authorize it to WRITE bothkeep’s andother’s current scopes – a caller can no longer reconcile, and thereby delete, another principal’s item merely by knowing its id.keepandothermust also share the samekind: merging e.g. atextitem into animageitem is never a legitimate reconciliation of the same knowledge, so this is a hard reject with no override. This is deliberately not a full lineage-graph check (nolinks/common-ancestor requirement): P1’s own cross-team-fork use case reconciles items that were never formally linked to each other; kind agreement plus dual-scope write authorization is the minimum coherent gate that rejects genuinely unrelated items (different kind, unauthorized principal) without breaking that legitimate use case.prefer(unrecognized values now raiseValueErrorinstead of silently behaving like"keep") and both items’ pre-merge states are persisted as immutable, content-addressed revisions (MXR-080-0265) beforekeepis overwritten andotheris deleted – recoverable withrevision()even thoughotherno longer exists as a live item. The delete is real and permanent as a LIVE item, so it requires an explicitconfirm=Trueacknowledgment; without it this raisesValueErrorrather than silently performing the destructive step.
- class Space(substrate, team, *, shared=(PUBLIC,), policy=None)[source]
Bases:
objectA team’s scoped view over a shared substrate: its own items plus what has been shared to it.
teamdoubles as this space’s authorization principal (MXR-080-0264): its home scope, perAccessPolicy, is the scope of the same name. Construction validates every entry ofsharedagainstpolicy– PUBLIC always passes; anything else, including another team’s scope, needs an explicit grant, or construction raisesAccessDeniedErrorrather than silently trusting the caller-supplied tuple. Reads re-check live (seescopes), so revoking a grant after construction takes effect immediately rather than requiring a newSpace.- property scopes: set[str]
its own scope plus whichever of
sharedthe policy currently authorizes – re-checked live (not just once at construction), so a grant revoked later stops being visible immediately rather than requiring a newSpace.- Type:
Scopes actually visible to this team right now
- all(*, kind=None)[source]
Every visible item (optionally of one kind) – never another team’s private knowledge.
- add(*, scope=None, **kw)[source]
Add an item to this team’s own scope by default (pass
scope=PUBLICto share immediately, or any other scope this principal holds a write grant for). MXR-080-0264: no longer writes into an arbitrary scope – in particular another team’s – on the caller’s say-so alone.
- retrieve(query, *, k=8, **kw)[source]
Retrieve over exactly the team’s visible set (own scope ∪ shared), with cross-kind diversity.
- publish(ids, *, to=PUBLIC, by=None)[source]
Share this team’s items into a common scope (audited). Only own-scope items are publishable.
by(default:self.team) is also the authorization principal for this call (MXR-080-0264): overriding it to a different principal requires that principal to hold its own grants onself.policy, sinceSpacecannot vouch for an identity other than the team it represents.