Skip to content

symfonic.platform.values

values

What crosses the platform boundary: credentials in, a principal out.

TRN-3 is the rule these types exist to satisfy — no port signature mentions fastapi.Request, Response or HTTPException, which is what makes a second host (a CLI serve, gRPC, a serverless entry point) a new adapter rather than a second copy of the platform.

AuditRecord dataclass

AuditRecord(action: str, outcome: str, principal_id: str, scope_key: str, resource_type: str = 'tenant', resource_id: str = '', metadata: Mapping[str, Any] = dict(), at: float = time.time())

One append-only audit fact (AUD-1..3).

The credential screen runs in __post_init__ rather than in the sink, so a record that would leak cannot be constructed — a sink-side filter only protects the sinks that remember to run it.

AuthenticatedPrincipal dataclass

AuthenticatedPrincipal(principal_id: str, scope: SubjectScope, is_admin: bool = False, derivation: Mapping[str, Any] = (lambda: MappingProxyType({}))())

SCOPE-2 — whole, or not produced at all.

There is no partially-derived principal and no mutation after derivation. Code that needs a narrower scope derives a child by :meth:narrowed, which refuses to widen, rather than by editing the one it was given.

narrowed

narrowed(child: SubjectScope) -> AuthenticatedPrincipal

SCOPE-9 / LAY-ADR §3.3: children narrow, compilation rejects widening.

Source code in src/symfonic/platform/values.py
def narrowed(self, child: SubjectScope) -> AuthenticatedPrincipal:
    """SCOPE-9 / LAY-ADR §3.3: children narrow, compilation rejects widening."""
    if not child.narrows(self.scope):
        raise AuthorizationError(
            f"{child.scope_key!r} does not narrow {self.scope.scope_key!r}; a "
            "child scope is a subset of its parent's, never a sibling and "
            "never a widening"
        )
    return AuthenticatedPrincipal(
        principal_id=self.principal_id,
        scope=child,
        is_admin=self.is_admin,
        derivation=self.derivation,
    )

require_self

require_self(tenant_id: str) -> None

SCOPE-8 / ADM-4: a self-scope route cannot be redirected.

Refused, not honoured, and refused before the service is called — a parameter, body field, or header naming another tenant is an attempt, not a preference.

Source code in src/symfonic/platform/values.py
def require_self(self, tenant_id: str) -> None:
    """SCOPE-8 / ADM-4: a self-scope route cannot be redirected.

    Refused, not honoured, and refused *before* the service is called — a
    parameter, body field, or header naming another tenant is an attempt,
    not a preference.
    """
    if tenant_id != self.scope.tenant_id:
        raise AuthorizationError(
            f"this operation acts on the resolved tenant "
            f"{self.scope.tenant_id!r}; a request naming {tenant_id!r} is "
            "refused rather than redirected (SEC-AUTHZ-2)"
        )

RequestCredentials dataclass

RequestCredentials(headers: Mapping[str, str] = dict(), peer: str | None = None, user_agent: str | None = None, route: str = '')

Everything a resolver may look at, and nothing a handler would add.

A frozen mapping plus the connection facts. Header lookup is case-insensitive because HTTP header names are, and a resolver that only matched X-Tenant-ID exactly would authenticate one proxy and refuse another for no reason a user could see.