symfonic.agent.fastapi.transport.codec¶
codec ¶
InvocationCodec (port registry row 17) — TRN-7.
/chat, /stream, /stream/typed, and /resume are four projections
of one invocation path (TRN-4). They share this codec because four handlers —
and, the moment a second host exists, more than four — map the same shape.
The CRUD routes deliberately do not: for those the pydantic model already is
the codec, and interposing a second one would be ceremony with no consumer.
Nothing here performs I/O or reads ambient state: decode_invocation is a
pure projection of a validated request plus an already-derived scope, which is
what lets the same values feed a CLI or a test recorder unchanged.
InvocationCodec ¶
Decode the wire input; encode the result. Sync, pure, total.
decode_invocation ¶
decode_invocation(payload: Any, *, scope: Any, is_admin: bool = False, principal_id: str | None = None) -> InvocationRequest
Project a validated AgentRequest onto an invocation value.
Identity fields present on the shipped AgentRequest model
(tenant_id) are ignored, not merged: the scope is whatever the
resolver derived. They are ignored rather than rejected because the
field is part of the frozen request model and the shipped clients send
it (TRN-5 outranks TRN-8's "rejected" wording here; the security
property — identity never comes from the body — is identical).
principal_id falls back to the resolved scope's tenant when the host
authenticated nobody by name. That is the convention
:meth:~symfonic.platform.scope.HeaderScopeResolver.resolve already
ships — facts.get("principal_id") or tenant_id — reused rather than
re-decided, so an admin claim always names some resolved subject and
never the empty string. It is still never read from the body.
Source code in src/symfonic/agent/fastapi/transport/codec.py
encode_result ¶
Encode a facade result for the wire.
/chat declares response_model=AgentResponse, so FastAPI already
owns the serialization and the codec must not double-encode; returning
the value unchanged is the encoding. The method exists so a second host
has one place to put its own.
Source code in src/symfonic/agent/fastapi/transport/codec.py
InvocationRequest
dataclass
¶
InvocationRequest(query: str, scope: Any, is_admin: bool = False, session_id: str | None = None, attachments: Sequence[Any] | None = None, principal_id: str = '')
The transport-neutral invocation value the facade is called with.
scope and is_admin come from the resolved principal and never from
the body (TRN-8). session_id is an opaque string that round-trips
verbatim, colons and all (TRN-9) — transport does not parse or re-key it.
admin_claim ¶
The claim a host binds around the call, or None for no claim.
Built here rather than in each handler so the three invocation routes
cannot bind three different things. None when the request is not an
administrator's, because binding "not an admin" and binding nothing are
the same fact and the second one costs no object.
Source code in src/symfonic/agent/fastapi/transport/codec.py
as_kwargs ¶
Keyword arguments for the facade's invocation methods.
is_admin is deliberately not among them (TA8.43, C1-K). The
keyword was replaced on the 11.0 line: the facade refuses
is_admin=True by name, because a bit the calling code passes is a
bit the calling code chose, and the authority that gates the tenant
budget breaker has to come from an authenticated principal instead. The
field stays on this value because it is the resolved principal's fact
(TRN-8) and it is what the host binds -- see
:func:~symfonic.agent.cutover.authority.bind_admin_authority and the
handlers in symfonic.agent.fastapi.router. Dropping the field would
have lost the fact; passing it on would have kept the retired keyword.