Skip to content

symfonic.capabilities.memory.scope

scope

The memory scope: three levels, one separator, and a total visibility rule.

Every port signature in this capability is keyed by a scope, and the reason is SEC-TEN-5: a backend must be able to enforce tenant isolation without importing the platform that derived the tenant. A scope value that carries its own hierarchy is what makes that possible — the backend answers "is this memory visible here?" by comparing segments, not by asking anyone.

Visibility runs one way only. A memory written at acme is visible inside acme/alice/s1, because a tenant-wide fact is true of every session in the tenant. A memory written at acme/alice/s1 is not visible at acme: promoting session content upward is a consolidation decision (T3.3.3), and a retrieval that did it implicitly would leak one user's session into another's.

The charset is not cosmetic. / separates levels, so a segment containing one forges a level — MemoryScope("acme/evil") would otherwise claim to be a tenant while comparing equal to a descendant of acme.

MemoryScope dataclass

MemoryScope(tenant: str, principal: str = '', session: str = '')

Where a memory lives: tenant, then principal, then session.

The levels are positional and gapless. A session without a principal is a hole in the hierarchy — it would compare as a child of the tenant while naming something the tenant cannot enumerate — so it is refused at construction rather than normalised into something plausible.

path property

path: str

The canonical string form. Stable, and safe to use as a store key.

segments property

segments: tuple[str, ...]

The populated levels, outermost first.

covers

covers(other: MemoryScope) -> bool

Whether a memory written at self is visible at other.

Segment-wise, never string-prefix: acme does not cover acmecorp, and a rule written with :meth:str.startswith would say it does.

Source code in src/symfonic/capabilities/memory/scope.py
def covers(self, other: MemoryScope) -> bool:
    """Whether a memory written at ``self`` is visible at ``other``.

    Segment-wise, never string-prefix: ``acme`` does not cover ``acmecorp``,
    and a rule written with :meth:`str.startswith` would say it does.
    """
    mine = self.segments
    theirs = other.segments
    return len(mine) <= len(theirs) and theirs[: len(mine)] == mine

distance

distance(other: MemoryScope) -> int

Levels from self down to other, or -1 when not covered.

-1 rather than an exception: distance is asked once per candidate during ranking, and "not visible from here" is an ordinary answer there.

Source code in src/symfonic/capabilities/memory/scope.py
def distance(self, other: MemoryScope) -> int:
    """Levels from ``self`` down to ``other``, or ``-1`` when not covered.

    ``-1`` rather than an exception: distance is asked once per candidate
    during ranking, and "not visible from here" is an ordinary answer there.
    """
    if not self.covers(other):
        return -1
    return len(other.segments) - len(self.segments)

validate

validate() -> None

Refuse every scope this capability cannot compare.

Source code in src/symfonic/capabilities/memory/scope.py
def validate(self) -> None:
    """Refuse every scope this capability cannot compare."""
    if not self.tenant:
        raise MemoryContractError(
            "a memory scope must name a tenant; an unscoped memory is a memory no "
            "backend can isolate (SEC-TEN-5)."
        )
    if self.session and not self.principal:
        raise MemoryContractError(
            f"scope declares session {self.session!r} with no principal. The levels are "
            "positional: a session under an anonymous principal would compare as a direct "
            "child of the tenant, making it visible to every other principal in it."
        )
    for level, value in (
        ("tenant", self.tenant),
        ("principal", self.principal),
        ("session", self.session),
    ):
        if value and not _SEGMENT_CHARSET.match(value):
            raise MemoryContractError(
                f"{level} segment {value!r} is outside the permitted charset "
                f"[A-Za-z0-9_.:@-]. {SCOPE_SEPARATOR!r} separates levels, so a segment "
                "containing one forges a level of the hierarchy."
            )

scope_from_path

scope_from_path(path: str) -> MemoryScope

Parse the canonical path form back into a scope.

Source code in src/symfonic/capabilities/memory/scope.py
def scope_from_path(path: str) -> MemoryScope:
    """Parse the canonical path form back into a scope."""
    segments = path.split(SCOPE_SEPARATOR) if path else []
    if not 1 <= len(segments) <= 3:
        raise MemoryContractError(
            f"scope path {path!r} has {len(segments)} levels; a memory scope is "
            "tenant[/principal[/session]] — between one and three."
        )
    return MemoryScope(*segments)