symfonic.capabilities.extensions.trust¶
trust ¶
Boundary validation for untrusted extension payloads (AS-INT-1, AS-INT-2).
Everything an MCP server answers and everything a plugin returns arrives here before it becomes a contribution. The module is small on purpose: it is the one place that decides what "a name", "a description", and "a schema" mean, so a reviewer checking that the caps are sane reads one file rather than four call sites that each grew their own.
Three rules run through it.
Identifiers are rejected; content is truncated. A malformed name is
refused (:class:~.errors.UntrustedPayloadError) because names index
dictionaries, appear in rendered manifests, and route calls — a name outside the
charset can forge a delimiter. A too-long description is truncated with a
diagnostic, because it is content: dropping the tool over its docstring would
let a verbose server delete itself from a catalogue.
A schema is refused, never repaired. It drives argument binding, so a schema that is too deep, too large, or not a mapping is a schema the framework declines to bind against. Repairing one would mean the model is told about a tool whose real signature nobody checked.
Bounds exist for every unbounded field. AS-ING-6's ceiling argument applies to integration payloads too: a server that answers with a 300 MB result string must cost a truncation, not a process.
TrustLimits
dataclass
¶
TrustLimits(max_identifier_length: int = 128, max_description_length: int = 8192, max_schema_bytes: int = 65536, max_schema_depth: int = 16, max_tools_per_server: int = 512, max_result_length: int = 262144, max_result_parts: int = 64)
The ceilings every untrusted payload is read under.
Defaults are generous enough that a well-behaved server never notices them and small enough that a hostile one cannot exhaust the process. They are a value rather than module constants so a deployment facing an unusually chatty server can raise one without patching the framework.
sanitize_text ¶
Return (text, truncated) for an untrusted content string.
A non-string reads as absent rather than raising: a server that answers
description: null has given us a tool without a description, which is
poor but not hostile. Control characters are stripped, then the result is
capped at limit and marked when it was shortened.
Source code in src/symfonic/capabilities/extensions/trust.py
validate_identifier ¶
validate_identifier(value: Any, *, field: str, origin: str, limits: TrustLimits, allow_dot: bool = True) -> str
Return value as a usable identifier, or refuse it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
Any
|
The raw payload field, of unknown type. |
required |
field
|
str
|
The field's name, for the error message. |
required |
origin
|
str
|
Who supplied it (server name, plugin name). |
required |
limits
|
TrustLimits
|
The ceilings to read it under. |
required |
allow_dot
|
bool
|
|
True
|
Raises:
| Type | Description |
|---|---|
UntrustedPayloadError
|
If it is not a string, is empty, exceeds the length cap, or leaves the identifier charset. |
Source code in src/symfonic/capabilities/extensions/trust.py
validate_schema ¶
Return the payload's input schema as a plain dict, or refuse it.
An absent schema is legal and reads as {} — an MCP tool taking no
arguments is ordinary. Anything present must be a JSON-shaped mapping
within the depth and size bounds.
Raises:
| Type | Description |
|---|---|
UntrustedPayloadError
|
If the schema is not a mapping, is not JSON-serialisable, or exceeds a bound. |