symfonic.core.scope¶
scope ¶
TenantScope — immutable hierarchical N-level multi-tenant request scope.
v8.0 introduces a root-first ordered path of :class:ScopeLevel values
as the canonical hierarchical representation (org → brand → conversation,
arbitrary depth). The legacy flat fields (tenant_id / sub_tenant_id /
namespace / inherits_from) are RETAINED as real fields so that every
existing read site (224 in src/, 462 test constructions) and the byte-identical
to_state_dict / equality / frozen contracts keep working unchanged.
The path is computed:
- When no explicit path is supplied, it is DERIVED from the flat fields
(a 1- or 2-level path) — backward-compatible.
- When an explicit N-level path is supplied (via :meth:from_path /
:meth:root / :meth:child), it is stored and the flat tenant_id /
sub_tenant_id are computed back off it for the legacy read sites.
The materialised scope_path string (US-delimited, root-first) and the
:meth:ancestor_prefix_paths IN-list are the backend isolation primitives
(design §5.b) — exact-IN over ancestor prefixes, NEVER LIKE 'prefix%'.
ScopeLevel ¶
Bases: BaseModel
One level in a hierarchical :class:TenantScope path.
kind is an adopter-defined label ("org" / "brand" /
"conversation" / "tenant" …) — deliberately NOT an enum so that
arbitrary hierarchies (org/team/project/session) are expressible
(design §3). id is the identifier at this level. Both must be
non-empty and MUST NOT contain the path delimiter (a delimiter inside an
id would forge a false level boundary — the §5.b false-prefix class).
TenantScope ¶
Bases: BaseModel
Immutable hierarchical N-level multi-tenant request scope.
Canonical representation is the root-first path (path[0] broadest,
path[-1] most-specific). The legacy tenant_id / sub_tenant_id
fields are RETAINED for backward compatibility and, when no explicit path
override is supplied, ARE the 1-/2-level path. When an explicit N-level
path is supplied via the factories, tenant_id / sub_tenant_id read
back off the path.
Isolation (design §5.d): a query at path P sees a memory at path Q iff Q is
a prefix of P. Enforced via :meth:ancestor_prefix_paths exact-IN at the
backend, NEVER a string prefix scan.
path
property
¶
Root-first ordered path (canonical hierarchical representation).
Derived from the flat fields when no explicit override is set
- tenant_id only → 1-level
(tenant:tenant_id,) -
- sub_tenant_id → 2-level
(tenant:tenant_id, sub_tenant:sub)
- sub_tenant_id → 2-level
scope_depth
property
¶
Number of levels in the path (1-based; root-only path is depth 1).
scope_path
property
¶
Materialised US-delimited, root-first path string for the backend.
org\x1facme\x1fbrand\x1fmyhalos\x1fconversation\x1fc-123.
This is the stored column/field value (design §5.b).
ancestor_prefix_paths ¶
The exact-IN list for the isolation filter (design §5.b/§5.d).
Returns the materialised scope_path string of EVERY prefix of this
scope's path (root → … → self), inclusive. A query at path P uses
this list as WHERE scope_path = ANY(list) so it matches a stored
memory at path Q iff Q is a prefix of P — exact set membership, never
a LIKE 'prefix%' scan (which would leak acme-corp to acme).
Source code in src/symfonic/core/scope.py
child ¶
Return a new scope with one more (deeper) level appended.
org_scope.child("brand", "myhalos").child("conversation", "c-123")
builds the full hierarchy. Namespace and inherits_from are preserved.
The result preserves the receiver's concrete type (type(self)) so
chaining .child() off a subclass such as
:class:~symfonic.agent.types.FrameworkTenantScope keeps the subclass
(and its converter methods) instead of silently downcasting to the
base TenantScope (P1 — subclass-drop on chaining).
Source code in src/symfonic/core/scope.py
from_path
classmethod
¶
from_path(
path: list[ScopeLevel] | tuple[ScopeLevel, ...],
*,
namespace: str | None = None,
) -> TenantScope
Construct an N-level scope from an explicit root-first path.
Source code in src/symfonic/core/scope.py
from_request
classmethod
¶
from_request(
tenant_id: str,
*,
sub_tenant_id: str | None = None,
namespace: str | None = None,
path: list[ScopeLevel]
| tuple[ScopeLevel, ...]
| None = None,
) -> TenantScope
Construct a scope from request-edge fields (blessed adopter entry).
Supply path for N-level hierarchies, or the flat fields for the
legacy 1-/2-level shape.
Source code in src/symfonic/core/scope.py
from_state_dict
classmethod
¶
Reconstruct a :class:TenantScope from a state-dict mapping.
Accepts UPPER_SNAKE and lower_snake casings (UPPER wins). Reads an
explicit path / PATH key (list of {kind, id} dicts) when
present — the N-level form; otherwise derives from the legacy flat
fields. inherits_from is read recursively (v7.22 compat).
Source code in src/symfonic/core/scope.py
narrow ¶
Create a new scope narrowed to a sub-tenant (legacy 2-level shim).
Retained byte-identically from v7.22: sets both sub_tenant_id and
namespace to the supplied value and preserves inherits_from.
For N-level hierarchies prefer :meth:child.
Source code in src/symfonic/core/scope.py
root
classmethod
¶
Construct a 1-level root scope (kind:id,).
to_state_dict ¶
Convert to state-dict fields for BaseAgentState injection.
Backward-compatible: a legacy 1-/2-level scope (no explicit path
override) emits EXACTLY the pre-v8.0 keys
(tenant_id + sub_tenant_id [+ namespace] [+
inherits_from]) — byte-identical. An explicit N-level path
additionally emits a path key (list of {kind, id} dicts) so
the hierarchy round-trips.
Source code in src/symfonic/core/scope.py
is_prefix_path ¶
Return True iff stored is one of the query's ancestor prefixes.
The exact-set-membership isolation primitive (design §5.d). stored is
a node's materialised scope_path; query_prefixes is the query's
:meth:TenantScope.ancestor_prefix_paths. Exact membership — NEVER a
startswith (which leaks acme-corp into an acme query).
Source code in src/symfonic/core/scope.py
legacy_scope_path ¶
Backfill string for a pre-v8.0 tenant_id-only row (design §6.b).
Maps to a 1-level root path tenant\x1f<tenant_id> so old rows behave
as tenant-global memories visible to every descendant query under that
tenant — the correct conservative default (they were tenant-global before).
Source code in src/symfonic/core/scope.py
scope_path_for ¶
Materialise a root-first path into the US-delimited storage string.
(org:acme, brand:myhalos) → org\x1facme\x1fbrand\x1fmyhalos.