graflo.architecture.query.caps¶
Hard limits on what a read request may ask for.
Caps live in core, not in route handlers, so that no surface can opt out of them by forgetting to check. A handler that skips a check is a silent hole; a model that will not construct is not.
The central decision here is that :class:QueryCaps is not a field on any
query model. If it were, a request body could carry {"caps": {"max_hops": 99}}
and "enforced in core" would be false by construction. Queries validate against
the module-level :data:HARD_CAPS, and :meth:GraphQuery.narrowed is the only
way to change a limit — downward.
Attributes¶
HARD_CAPS = QueryCaps()
module-attribute
¶
Classes¶
CapExceededError
¶
Bases: ValueError
A query asked for more than a cap allows.
Carries the cap's name so the surface can say which limit was hit rather than returning a generic validation failure — an agent that is told "too many hops, max is 3" can retry; one told "invalid request" cannot.
Source code in graflo/architecture/query/caps.py
QueryCaps
¶
Bases: ConfigBaseModel
Ceilings a read request must fit inside.
Defaults are the core ceiling. A deployment narrows them per connection via
:meth:GraphQuery.narrowed; nothing widens them.
Source code in graflo/architecture/query/caps.py
Attributes¶
max_edge_types = PydanticField(default=20, ge=1, description='Most distinct relations one request may name.')
class-attribute
instance-attribute
¶
max_elements = PydanticField(default=5000, ge=1, description='Most vertices plus edges a response may carry.')
class-attribute
instance-attribute
¶
max_hops = PydanticField(default=3, ge=1, description='Deepest traversal permitted.')
class-attribute
instance-attribute
¶
max_rows = PydanticField(default=1000, ge=1, description='Most rows a single query may return.')
class-attribute
instance-attribute
¶
max_seeds = PydanticField(default=10, ge=1, description='Most anchor vertices a traversal may start from.')
class-attribute
instance-attribute
¶
projection_allow_list = PydanticField(default=None, description='Property names a response may include. None means unrestricted; an empty list means nothing may be projected, which is not the same thing.')
class-attribute
instance-attribute
¶
timeout_s = PydanticField(default=30.0, gt=0, description='Longest a query may run.')
class-attribute
instance-attribute
¶
Methods:¶
narrow(other)
¶
Combine two cap sets, taking the stricter of each.
Narrowing is a lattice meet, not an override: a policy that tried to raise a ceiling silently becomes a no-op rather than a privilege escalation.