symfonic.platform¶
platform ¶
The platform layer: derive who is calling, decide whether it may proceed.
PLAT-ADR's one-sentence rule — the platform derives who the caller is, decides whether the request may proceed, and hands the request to the public facade; it implements nothing an invocation does — is what every module here is measured against.
T4.1.3 lands three of PLAT-ADR's five services, all transport-neutral (TRN-3):
- :class:
~symfonic.platform.scope.HeaderScopeResolver— the single derivation point (SCOPE-1), constructed and injected rather than registered in a module global, so two agents in one process no longer share one verifier; - :class:
~symfonic.platform.budget.BudgetService— admission through the narrowBudgetCheckport with a typed refusal, retiring thestr(exc).startswith("Budget exceeded:")branch and the raw-header read; - :class:
~symfonic.platform.privacy.PrivacyServiceand :class:~symfonic.platform.erasure.ErasureSaga— export assembly and a durable, resumable, fail-closed erasure saga over the runtime-serviceSubjectDataStoreregistry.
Not here, by design: transport. No module in this package imports fastapi,
constructs a response, or names a status code. Routers are T4.1.2's, and they
will be mappers over these services (TRN-1) rather than the 3,759 lines of
handler that reach into engine privates 54 times today.
AdmissionGate ¶
AdmissionGate(*, resolver: ScopeResolver, guard: SubjectAdmission | None = None, budget: BudgetService | None = None)
The three platform-side gates, composed in the one declared order.
Each collaborator is injected. There is no service locator and no lookup by name: a gate receives the two or three objects it uses, which is what keeps the order auditable — you can read it in the constructor, not chase it through a registry.
Source code in src/symfonic/platform/admission.py
admit
async
¶
Run the gates in order and return the principal, or raise the first no.
Source code in src/symfonic/platform/admission.py
AgentComposer ¶
Bases: Protocol
Turns a scope and the process's shared resources into one Agent.
The adopter's composition root, expressed as a callable so the host never learns which capabilities a deployment folds. It receives the scope because capabilities are scope-bound at construction -- a memory capability is built for one tenant -- and that is precisely why an agent cannot be shared across scopes.
Synchronous on purpose. Composition assembles objects the host already opened; a composer that needed to await would be opening a resource of its own, which is the ownership this contract places elsewhere.
AgentHost ¶
AgentHost(*, composer: Any, resources: Any, open_resources: Any = None, continuation_factory: Any = None)
Owns the process's shared resources and the registry over them.
Built once by a composition root and closed once. open_resources is a
separate callable rather than a method on the resources object because
opening is not part of :class:~symfonic.platform.host.SharedResources:
an adopter may hand over something already open, and requiring an aopen
would make that a lie.
Source code in src/symfonic/platform/agent_host.py
aclose
async
¶
Close the registry, then the resources, exactly once.
In that order: an agent holding a closed pool is a worse failure than a pool held a moment longer, because the first surfaces mid-turn and the second surfaces nowhere.
Resources are closed only if they were opened. A host whose
construction failed before start is still closed by the handler,
and closing what was never opened is how a shutdown path raises during
a shutdown that was already going badly.
Source code in src/symfonic/platform/agent_host.py
resume
async
¶
Continue a durable human pause through this host's scoped agents.
A host with no configured continuation fails early instead of accepting
a token it cannot safely bind to a scope. Compose
human_continuation(...) with a verified claim-to-scope resolver.
Source code in src/symfonic/platform/agent_host.py
start
async
¶
Open shared resources once, however many times this is called.
Source code in src/symfonic/platform/agent_host.py
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.
AuditSeam ¶
AuditSeam(sink: AuditSink | None = None, *, on_degraded: Callable[[AuditRecord], None] | None = None)
The one path platform services write audit facts through.
Handlers do not construct sinks and do not reach for a module-level emitter; they are handed a seam. That is what makes "every administrative operation is audited before it mutates" (ADM-6) checkable rather than conventional.
Source code in src/symfonic/platform/audit.py
degraded
property
¶
True once a record failed to land. Sticky: it describes the host.
record
async
¶
Emit. On failure, degrade loudly — and for destruction, fail closed.
Source code in src/symfonic/platform/audit.py
AuditSink ¶
Bases: Protocol
Registry row 3 — narrow, append-only, and never constructed in a handler.
AuditSinkError ¶
AuthPosture
dataclass
¶
AuthPosture(environment: str, production: bool, verifier_registered: bool, insecure_override: bool, requests_served_untrusted: int = 0)
Continuously observable answer to "is this host trusting headers?".
Every field is something an operator would want on a dashboard, and
requests_served_untrusted is the one that turns a silent misconfiguration
into a rising number.
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 ¶
SCOPE-9 / LAY-ADR §3.3: children narrow, compilation rejects widening.
Source code in src/symfonic/platform/values.py
require_self ¶
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
AuthenticationError ¶
Bases: PlatformError
No derivable credentials. 401.
Never a fallback to a default, shared, or root tenant (SCOPE-4, SEC-FCP-1).
AuthorizationError ¶
Bases: PlatformError
Authenticated, but not for this scope. 403.
Also raised by :meth:AuthenticatedPrincipal.narrowed on an attempted
widening and by require_self on a redirect attempt (SCOPE-8/SEC-AUTHZ-2),
because "you may not act on that tenant" is one answer however it was asked.
BudgetExceededError ¶
BudgetExceededError(message: str, *, code: str = 'budget_exceeded', reason: str | None = None, retry_after: int = 3600, scope_key: str = '')
Bases: PlatformError
Admission refused by the budget port. 429 + Retry-After.
This class is BUD-2's whole point. The shipped path raises a generic error
whose message starts "Budget exceeded:" and three routers branch on that
prefix; here the class carries the decision, code distinguishes
exhaustion from an unavailable ledger, and retry_after is a number
rather than a hard-coded header string in three places.
Source code in src/symfonic/platform/errors.py
BudgetService ¶
BudgetService(*, budget_check: BudgetCheck | None, cost_read_model: CostReadModel | None = None, audit: AuditSeam | None = None)
Admission from the narrow port, plus the cost read models.
Source code in src/symfonic/platform/budget.py
admit
async
¶
The decision, without raising. enforce is the gate.
Split because BUD-1 has two consumers: the platform admits a request,
and the invocation re-checks before its own effects. Both read the same
:class:BudgetDecision; only the first turns it into a refusal status.
Source code in src/symfonic/platform/budget.py
describe ¶
BUD-7: what this host does about budgets, stated rather than implied.
enforce
async
¶
SCOPE-14 step 5: fail-closed, evented, typed.
A broken ledger denies rather than admits (SEC-FCP-1). The alternative — "the accountant is down, so everything is free" — is the failure mode a budget exists to prevent.
Source code in src/symfonic/platform/budget.py
summarize
async
¶
BUD-6: a projection, scoped to the caller like every other route.
Returns None when no read model is bound — an absent projection, not
an empty one. Reporting zeros for a ledger nobody is keeping is how a
cost dashboard learns to lie.
Source code in src/symfonic/platform/budget.py
ContinuationService ¶
Bases: Protocol
Resumes what a pause stopped, over the host's own agents and stores.
Deliberately not a method on Agent. An agent is bound to one scope and
does not know which of its turns are outstanding; resuming needs a token
to be looked up, a scope to be recovered from it, and the agent for that
scope to be found -- three things the host has and an agent does not.
resume
async
¶
Continue the paused turn with answer and return its result.
Idempotency is the contract's hardest clause and it belongs here rather than in a transport: a person answering twice, or a client retrying, must not run the rest of the turn twice. An implementation either completes once and replays the result, or refuses the second attempt by name -- and it must say which, because "the answer was accepted" and "the answer was accepted again" look identical to a caller otherwise. The supported human adapter chooses at most once: it prepares the scoped agent before redemption, then rejects a second use after the durable claim; it does not replay effects if the resumed invocation itself later fails.
Raises:
| Type | Description |
|---|---|
UnknownPauseToken
|
if the token names no resumable turn. |
Source code in src/symfonic/platform/continuation.py
ticket
async
¶
The paused turn token names.
Raises:
| Type | Description |
|---|---|
UnknownPauseToken
|
if it names none, or has expired. |
ContinuationUnavailable ¶
Bases: RuntimeError
Raised when a host was not composed with durable continuation wiring.
DecisionLog
dataclass
¶
The in-process sink, for a deployment that wants one and nothing more.
Not a default: governance() records nothing unless handed a sink, so
a deployment that never asked for a decision log does not accumulate one
for the life of the process.
by_rule ¶
Every decision one rule took part in.
The question a dashboard asks, and the reason rule_ids is a tuple
rather than a string: a decision two rules made is found by either.
Source code in src/symfonic/platform/governance_decisions.py
DecisionSink ¶
Where decisions go. A protocol in all but name.
Anything with record(decision) is one, so a deployment writes to its
own store without inheriting from this package.
DestructiveConfirmationError ¶
Bases: PlatformError
A destructive operation arrived without intent bound to the resolved scope.
ADM-5: the confirmation is compared against the derived tenant id, never against a value taken from the same request that supplied the confirmation — otherwise the check confirms only that the caller can copy a string.
ErasureOutcome
dataclass
¶
ErasureOutcome(scope_key: str, complete: bool, counts: dict[str, int] = dict(), unconfirmed: tuple[str, ...] = (), errors: dict[str, str] = dict(), generation: int = 0)
What one pass of the saga achieved. Partial success is a real answer.
ErasureSaga ¶
ErasureSaga(*, registry: SubjectDataStoreRegistry, fence: ErasureFence, saga_store: ErasureSagaStore, audit: AuditSeam | None = None, max_attempts: int = 3)
Drives every registered participant to a confirmed absence.
Source code in src/symfonic/platform/erasure.py
run
async
¶
One bounded pass. Idempotent, and safe to call again after a crash.
Source code in src/symfonic/platform/erasure.py
ExtensionCapability ¶
The composed extensions, in the shape Agent accepts.
Source code in src/symfonic/platform/extensions.py
composed
property
¶
The merged extension set, for a caller that wants the diagnostics.
sources
property
¶
The extensions' prompt fragments, for PromptingCapability.
capabilities=[
exts,
PromptingCapability(sources=[*persona, *exts.sources]),
]
Every fragment is declared untrusted. A plugin's text is not the deployment's own, and rendering it verbatim at an authored tier is how an extension writes instructions nobody in the deployment approved.
aclose
async
¶
Run the teardown hooks the composed extensions declared.
Called by whatever owns this capability -- the host, for a generated
app. teardown is a kernel-owned phase, so a capability cannot
register a stage there; the hook reaches its moment through the close
path instead. Every hook runs even if an earlier one raises, because a
server left open by a failed close is worse than a traceback.
Source code in src/symfonic/platform/extensions.py
contribute ¶
Offer the extensions' tools to the turn.
Tools only. CapabilityContribution carries tools, stages, handlers
and grants -- there is no field for a prompt contribution, because the
prompt is compiled by the prompting capability from sources. So the
fragments come out of :attr:sources instead and go where every other
source goes, which is the same answer the knowledge door reached.
request is read for its grants and found to need none: an extension
contributes what it declared and performs no effect of its own.
Source code in src/symfonic/platform/extensions.py
ExtensionContractError ¶
Bases: ExtensionError
A contribution's own declaration is malformed.
Raised at declaration time — when the contribution value is validated — not at use time. A tool with no name is not a tool that fails when called; it is a tool that should never have been admitted to the catalogue.
ExtensionContribution
dataclass
¶
ExtensionContribution(extension: str, tools: tuple[ToolContribution, ...] = (), prompts: tuple[PromptFragment, ...] = (), policies: tuple[PolicyContribution, ...] = (), lifecycle: tuple[LifecycleContribution, ...] = ())
Everything one extension offers, in one value.
An extension is asked, it answers, and the answer is validated, ordered, and refused as a unit. Nothing here holds a reference to an engine, a registry, or a graph, which is why an extension cannot install itself.
build
classmethod
¶
build(extension: str, *, tools: Sequence[ToolContribution] = (), prompts: Sequence[PromptFragment] = (), policies: Sequence[PolicyContribution] = (), lifecycle: Sequence[LifecycleContribution] = ()) -> ExtensionContribution
Build a bundle from any sequences, freezing each into a tuple.
Source code in src/symfonic/capabilities/extensions/contracts.py
validate ¶
Validate the bundle, every member, and the attribution of each.
Attribution is checked here rather than on each member because it is a property of the pair: a well-formed tool contributed under someone else's name is exactly the confused-deputy shape AS-INT-5 is about, and the member alone cannot see that it was misfiled.
Source code in src/symfonic/capabilities/extensions/contracts.py
ExtensionProvider ¶
Bases: Protocol
What the composer asks. Two members, both deliberate.
name is a declared member so an isinstance check requires it: an
anonymous provider produces contributions nobody can revoke.
contribute is synchronous. Anything an extension needs to discover
over the network happens before composition — the MCP adapter's
discover() is its own async step — so composition itself is a pure
function over values, replayable and comparable without an event loop.
GovernanceDecision
dataclass
¶
GovernanceDecision(state: str, phase: str, stage: str, rule_ids: tuple[str, ...], call_id: str, tool: str, reason: str, at: datetime = (lambda: datetime.now(UTC))())
One governance outcome for one call.
state is the vocabulary, and it is three words on purpose:
applied
A rule rewrote the call and the tool ran with the rewrite.
refused
A rule objected and the call did not run.
discarded
A rule asked for a change this phase has no way to make. It is
neither of the above and must not be reported as either: an operator
reading applied for a change that never happened is worse off
than one reading nothing.
GovernanceRefused ¶
GovernanceRefused(*, stage: str, phase: str, disposition: Disposition, reason: str, rule_ids: tuple[str, ...] = ())
Bases: ContractViolationError
A governance stage refused, and the turn must stop.
Raised rather than returned as rejected(...), and the distinction is
the kernel's rather than a style choice. REJECTED means this
contribution was not applied -- a well-formed answer the dispatcher
declined, after which the turn continues; tests/kernel pins that
("a rung that ended the turn on a rejection would make refusal and failure
the same event"). What governance means is do not proceed, and the
kernel's word for that is an exception, which every phase already turns
into a stopped turn through require_no_crashed_stage.
The first version of this door used rejected for both. It read as a
refusal in the trace and the tool call ran anyway -- ten rounds refused,
ten calls executed -- because nothing consumes REJECTED and nothing was
supposed to.
Source code in src/symfonic/platform/governance_refusal.py
GovernanceStages ¶
A composable wrapper around a built governance pipeline.
The object a composition root passes to Agent(capabilities=[...]).
It holds the capability rather than subclassing it: governance answers
"what does this turn get, and why", and this answers "which rungs does that
run on" -- two questions, and merging them is what would put kernel imports
back inside the contained package.
Source code in src/symfonic/platform/governance.py
capability
property
¶
The governed pipeline, for a caller that wants the trace directly.
contribute ¶
Declare the rungs this pipeline runs on, and the handlers for them.
Classification and reflection may call a model and declare that effect when composed. Refusal and deterministic checks need no additional effect grant.
Source code in src/symfonic/platform/governance.py
GrantEffects ¶
An adopter's explicit authorization, written at the composition root.
Usage, and the whole point of the shape::
Agent(provider, capabilities=[
GrantEffects("memory-read"),
MemoryCapability(bridge),
])
It is not a capability. It contributes no stage, no handler and no tool; the facade recognises it by exact type, takes its families, and removes it from the sequence before folding. That ordering is the security property: a contribution can only ever narrow a set that was established without its participation, and it cannot manufacture another top-level entry.
Why a separate type rather than an Agent parameter: FAC-4 freezes the
facade at four constructor arguments, and extension happens through
capabilities=. Why not "installing the capability implies the grant":
that conflates installation with authorization and hides the authority from
the place a reviewer looks for it.
The honest limit, stated rather than papered over: nothing stops third-party
code from instantiating this type. The boundary is that the composition
root chose the top-level sequence. If untrusted code controls that sequence,
nominal framework provenance cannot recover authority — that needs policy
outside this process. And a granted family is granted for the whole
invocation, not to one capability: once memory-read is granted, any
capability in the same list may use it. Per-capability authority would need
grants keyed by capability rather than today's frozenset[str].
Source code in src/symfonic/kernel/contracts/effects.py
HeaderScopeResolver ¶
HeaderScopeResolver(*, verifier: TenantAuthVerifier | None = None, environment: str | None = None, allow_insecure_prod: bool = False, environ: dict[str, str] | None = None)
Registry row 16, default implementation: headers plus an injected verifier.
X-Tenant-ID on its own is untrusted input — the framework cannot assume
an auth model — so the verifier is what turns a claim into a principal. With
no verifier the header is accepted on trust, which is fine for a unit test
and never for production; :meth:posture is how that state stops being
invisible (SCOPE-5).
Source code in src/symfonic/platform/scope.py
posture ¶
A fresh reading every call — this is a gauge, not a one-shot log.
Source code in src/symfonic/platform/scope.py
resolve
async
¶
SCOPE-1: exactly once per request, and this is the once.
Source code in src/symfonic/platform/scope.py
HostClosed ¶
Bases: RuntimeError
Raised when an agent is requested from a host that has been closed.
A distinct type rather than a bare RuntimeError because the caller's
correct response differs: a closed host during shutdown is expected and
should end the request, while a closed host during normal operation is a
lifecycle bug in the composition root. A message alone cannot be branched
on without matching strings, which is the pattern this codebase has been
removing.
HostContinuationFactory ¶
Bind host-owned agent lookup after a deployment has composed its ports.
Source code in src/symfonic/platform/human_continuation.py
HumanContinuationService ¶
HumanContinuationService(*, capability: Any, scope_for_claims: Callable[[PauseClaims], Any], agent_for: Callable[[Any], Awaitable[Any]])
Redeem durable human pauses and route the resulting turn through a host.
scope_for_claims receives only claims that passed signature and expiry
verification. It must resolve the deployment's durable scope record and
may return None when the scope is no longer known; that is rejected
before a capability or agent is asked to continue the turn.
Source code in src/symfonic/platform/human_continuation.py
MountedRoute ¶
Bases: tuple
One served endpoint: (path, methods), comparable and printable.
NullAuditSink ¶
Drops records. The default only because a host that wants none says so.
ObservabilityServices
dataclass
¶
ObservabilityServices(collector: Any, metrics: Any = None, budget: Any = None, event_sinks: tuple[Any, ...] = ())
What a process needs to account for what its agents did.
Satisfies :class:~symfonic.platform.host.SharedResources structurally, so
a composition root hands this to the host it already builds rather than
installing it anywhere.
There is deliberately no setter. Ownership runs one way: the host holds these, and nothing installs them into a place other code discovers.
budget_is_shared
property
¶
Whether two replicas would enforce one limit or two halves of it.
Exposed because in-memory is a legitimate choice and a silent one is not: a deployment may accept per-replica budgets, but it may not be unable to tell that is what it has.
aclose
async
¶
Release what these own. Idempotent, because shutdown paths repeat.
Source code in src/symfonic/platform/observability.py
PauseTicket ¶
Bases: Protocol
What a paused turn hands back, and what resuming it needs.
The token is the whole of the caller's half. Everything else on this protocol is recovered from it rather than supplied: the scope the pause was issued under, the session it belongs to, and what the turn was waiting for. A caller that supplied any of them could contradict the token, and the contradiction would have to be resolved by trusting one of them.
question
property
¶
What the turn stopped to ask. Shaped by whoever registered the pause point; the platform does not interpret it.
PlatformAgentHost ¶
Bases: Protocol
Owns the process's shared resources and the registry over them.
The composition root a generated project's main builds once and closes
once. Everything a request needs comes from here; nothing a request does
happens here.
aclose
async
¶
Close the registry and then the shared resources, exactly once.
Idempotent, because shutdown paths run twice more often than they run cleanly: a lifespan that closes and an atexit that closes are both correct and both fire.
Source code in src/symfonic/platform/host.py
agent_for
async
¶
resume
async
¶
PlatformError ¶
PrivacyService ¶
PrivacyService(*, registry: SubjectDataStoreRegistry, fence: ErasureFence, saga_store: ErasureSagaStore, audit: AuditSeam | None = None, max_attempts: int = 3, bind_fence_audit: bool = True)
Export (Article 20) and erasure (Article 17), behind one boundary.
Source code in src/symfonic/platform/privacy.py
admit
async
¶
erase
async
¶
Article 17. Confirm, audit intent, tombstone, then sweep.
Source code in src/symfonic/platform/privacy.py
export
async
¶
PRIV-8 — exactly what the tenant's own queries could see.
Suppressed for a tombstoned subject like any other read: an export is not a privileged back door around the erasure it follows.
Source code in src/symfonic/platform/privacy.py
read_guard
async
¶
PRIV-5: reads are suppressed while any participant is unconfirmed.
resume
async
¶
Drive an interrupted erasure forward. Safe to call repeatedly.
This is what makes PRIV-3's "resumable" operational rather than theoretical: a scheduler, an operator, or a restart hook calls it, and the saga picks up exactly the participants that never confirmed.
Source code in src/symfonic/platform/privacy.py
PromptFragment
dataclass
¶
PromptFragment(fragment_id: str, text: str, extension: str, layer: str = 'l1', tier: str = 'session', scope: str = 'deployment', order: int = 100, truncated: bool = False)
Text an extension contributes to the compiled prompt.
The fields mirror the prompt compiler's contribution contract by name — layer, tier, scope, order — because the composition root's job is then a lookup rather than a translation. What it does not mirror is the tier range: a contributed fragment is restricted to the learned tiers, so no extension can place text where the model reads operator instruction.
Reflection
dataclass
¶
The critic's answer about the draft.
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.
ScopeDerivationError ¶
ScopeResolver ¶
Bases: Protocol
Registry row 16 — credentials to an authenticated principal.
The single derivation point (SEC-AUTHZ-1). Returns a whole principal or
raises; there is no third outcome, because a resolver that could return
None would make "no principal" a value a handler might forget to check.
ScopedAgentRegistry ¶
One agent per scope, composed on first use.
Concurrency is handled with a per-scope lock rather than one global lock, so a slow composition for one tenant does not serialise every other tenant's first request. The double check inside the lock is what makes the composer run exactly once: the first waiter composes, the rest find the entry already there.
Source code in src/symfonic/platform/agent_host.py
aclose
async
¶
Drop every agent. Idempotent, and does not touch shared resources.
Ownership runs one way: the registry borrows what the host opened, so closing it here would close a pool other registries may still hold -- and would do it from whichever one happened to close first.
Source code in src/symfonic/platform/agent_host.py
SharedResources ¶
Bases: Protocol
What every agent in a process borrows and none of them owns.
Deliberately opaque: the host holds it, hands it to the composer, and closes it. Naming its members here would make the host know what a deployment's memory stack is, which is the coupling this package exists to avoid -- an adopter with a different backend implements this and changes nothing else.
TelemetryCapability ¶
Reports each model round to the collector the host built.
Source code in src/symfonic/platform/telemetry.py
contribute ¶
Declare the post-model stage and the handler that reports the call.
request is read for its grants and found to need none. Counting a
call is not an effect on the turn: nothing downstream reads what this
stage writes, and a grant would be authority never exercised.
Source code in src/symfonic/platform/telemetry.py
TenantAgentRegistry ¶
Bases: Protocol
The scope -> agent mapping, and the only place agents are created.
Concurrency is part of the contract, not an implementation detail: two coroutines asking for the same scope at once receive the same agent, and the composer runs once. A registry that raced would give one tenant two agents with two capability sets over one store, and the second would silently win.
aclose
async
¶
agent_for
async
¶
The agent bound to scope, composing it on first use.
Raises:
| Type | Description |
|---|---|
HostClosed
|
if the registry has been closed. |
TenantAuthVerifier ¶
Bases: Protocol
The deployment's own answer to "is this caller allowed in this tenant?".
Returns a mapping of principal facts (principal_id, is_admin) or
None for a bare allow, and raises to deny. The admin bit comes from
here and nowhere else (SCOPE-7): the shipped X-Admin-Override header
convention is trusted only when a verifier has already authenticated the
caller, which makes it a verifier-side deployment convention — the one
place it can actually be reasoned about — rather than a transport rule.
ToolContribution
dataclass
¶
ToolContribution(name: str, extension: str, invoke: Callable[[Mapping[str, Any]], Awaitable[str]], description: str = '', input_schema: Mapping[str, Any] = dict(), origin: str = '')
One callable an extension offers the agent.
invoke is an async callable taking the bound arguments and answering a
string. It is captured at declaration time and never looked up again: the
legacy MCP provider routed each call through a mutable name→server dict, so
a later discovery could re-point an already-advertised tool at a different
server. Holding the callable makes that unrepresentable.
UnknownPauseToken ¶
Bases: LookupError
Raised when a token names no resumable turn.
A distinct type because the caller's correct response differs and cannot be derived from a message: an expired token is an ordinary outcome a transport turns into a 404 or a "this conversation moved on", while a malformed one is a bug in whoever minted it. Both are refusals, and only one is worth waking somebody for.
api_routes ¶
The subset under prefix. The question most callers are asking.
attribution_is_bound ¶
Whether these calls can be charged to a tenant at all.
False means the agent was composed without a scope, so every call is
recorded against the run and no per-tenant ceiling can ever fire. That is a
legitimate choice for a single-tenant deployment and a silent billing hole
for any other -- the same reason budget_is_shared exists next door.
Source code in src/symfonic/platform/telemetry.py
create_kernel_agent_router ¶
create_kernel_agent_router(host: Any, *, prefix: str = DEFAULT_PREFIX, memory: Any = None, graph: Any = None, scope_resolver: Any = None, memory_audit: Any = None) -> Any
A router that maps HTTP onto host.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
host
|
Any
|
anything with |
required |
prefix
|
str
|
where the routes mount. |
DEFAULT_PREFIX
|
memory_audit
|
Any
|
explicit acknowledged audit sink for point deletion. DELETE requires both this sink and a verified scope resolver; a missing or failing audit sink prevents storage mutation. |
None
|
scope_resolver
|
Any
|
what turns a request's credentials into an
authenticated principal, normally a
:class: |
None
|
graph
|
Any
|
an object with |
None
|
memory
|
Any
|
an object with |
None
|
Source code in src/symfonic/platform/transport.py
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 | |
fence_denial_recorder ¶
A recorder a row-14 fence can call on every denial.
Not destructive: the denial prevented a mutation, so failing the
already-refused write closed a second time would add nothing. The seam still
degrades loudly (AUD-4) if the sink is down.
Source code in src/symfonic/platform/fence_audit.py
mounted_routes ¶
Every endpoint app serves, whichever FastAPI version built it.
Walks included routers rather than trusting app.routes to be flat. A
route that carries no HTTP method -- a mount, a static files app -- is
skipped: this answers "what can be called", and those cannot.
Source code in src/symfonic/platform/routes.py
production_auth_gate ¶
HOST-5/HOST-6 — fail closed at startup, or log CRITICAL and continue.
Takes the resolver structurally (anything with posture()) so host
assembly can gate an adopter's own resolver, not only the shipped one. A
host that cannot build its auth path does not start in a degraded
"allow everything" mode.