laser_wire/limits.rs
1// Wire caps, enforced client-side so an oversized op fails fast instead of
2// round-tripping, and server-side so a hostile client cannot inflate state.
3
4/// Hard ceiling on rows in a single query reply. A `limit` above it is
5/// rejected with `QueryError::TooLarge`, and a `0` limit defaults to a full
6/// page. Callers page through larger result sets with `offset`.
7pub const MAX_PAGE_SIZE: usize = 1000;
8/// Page size a streaming reader pulls when the caller has not set an explicit
9/// limit. Large enough to amortize round-trips, small enough that an
10/// unbounded scan does not spike memory.
11pub const DEFAULT_STREAM_PAGE_SIZE: usize = 100;
12/// Hard cap on the number of `agdx.idx.*` headers a single record may carry.
13/// Total header byte size is already capped, but a buggy producer could stamp
14/// dozens of tiny indexed scalars under the byte budget and slow the
15/// projector. 32 covers every legitimate analytics row with head-room.
16pub const MAX_INDEX_ENTRIES_PER_RECORD: usize = 32;
17/// Cap on the payload bytes the projector **inlines into a materialized row**
18/// (when `inline_payload` is set). This bounds only the copy kept alongside the
19/// indexed row in the read-model backend, never the original message: the Iggy
20/// log always retains the full bytes and a fetch can replay them. Held at
21/// [`MAX_VALUE_BYTES`] (8 MiB) so a single inlined body and a single KV value
22/// share one "max opaque value" ceiling. A body above it still indexes and
23/// still lives in the log. It is just not duplicated into the row, so a typed
24/// fetch decodes from the log (or a claim-check [`ContentType::Ref`] body). The
25/// cap exists because multi-MB BLOBs per row bloat the embedded query DB and
26/// slow scans.
27///
28/// [`ContentType::Ref`]: crate::content::ContentType::Ref
29pub const MAX_PROJECTOR_PAYLOAD_BYTES: usize = MAX_VALUE_BYTES;
30
31// KV caps. Keys are arbitrary bytes (text, binary, anything), at
32// most 512 bytes. Values are arbitrary opaque bytes, capped at 8 MiB:
33// generous for session, flag, counter, cached-JSON, and chunked working state,
34// yet well under the frame ceiling so a set request and a get reply each ride
35// one socket frame with envelope head-room.
36/// Maximum KV key length, in bytes.
37pub const MAX_KEY_BYTES: usize = 512;
38/// Maximum KV value size, in bytes.
39pub const MAX_VALUE_BYTES: usize = 8 * 1024 * 1024;
40/// Hard ceiling on a KV scan page (LaserData Cloud clamps to it).
41pub const MAX_SCAN_LIMIT: usize = 1000;
42/// KV scan page size when the caller sets none.
43pub const DEFAULT_SCAN_LIMIT: usize = 100;
44/// The namespace a KV call without an explicit one binds to. A namespace is a
45/// logical bucket: keys are unique within it, scans are scoped to it, and one
46/// user's namespaces stay isolated from another's.
47pub const DEFAULT_NAMESPACE: &str = "default";
48/// Maximum KV (and memory) namespace length, in bytes. A namespace is a
49/// caller-chosen bucket name that flows into grant matching and scan scoping,
50/// so it is bounded and control-character-free, while its charset stays open
51/// (namespaces legitimately carry `/`-style hierarchy).
52pub const MAX_NAMESPACE_BYTES: usize = 128;
53
54// Fork caps. A fork id is a caller-chosen name (e.g. `"experiment-2026-q2"`),
55// so its length is a validatable input cap like a KV key: the client rejects an
56// over-long id before the round-trip. Per-deployment resource ceilings (how
57// many forks may exist) are NOT here: those are a managed-side policy a client
58// cannot validate against, surfaced only as a `ForkError`.
59/// Maximum fork id length, in bytes.
60pub const MAX_FORK_ID_BYTES: usize = 128;
61
62// Authorization caps. A role name is an admin-chosen identifier that flows
63// into grant matching, audit events, and console rendering, so it shares the
64// fork-id shape: a strict safelist plus a byte cap every tier validates the
65// same way. 64 matches the common RBAC-identifier norm.
66/// Maximum role name length, in bytes.
67pub const MAX_ROLE_NAME_BYTES: usize = 64;
68
69/// Ceiling on one `[len: u32 LE][bytes]` frame on the managed-command sockets,
70/// enforced by both the server and LaserData Cloud. A reply above it is replaced by
71/// a structured too-large error rather than truncated. The `u32` length prefix
72/// addresses far more (4 GiB), so this is a deliberate per-frame memory bound on
73/// the whole-frame buffer, not a transport limit. Every consumer of the managed
74/// sockets (the server-side dispatch, the streaming server sidecar, the reply-byte budget)
75/// MUST source this one constant rather than redefining its own frame cap, so
76/// the layers cannot disagree and a reply admitted by one is not rejected by the
77/// next. Changing it moves all of them in lockstep.
78pub const MAX_FRAME_BYTES: usize = 64 * 1024 * 1024;
79/// Hard ceiling on a single query reply's serialized bytes. A reply rides the
80/// managed-command socket as one `[len: u32 LE][bytes]` frame, buffered whole,
81/// so it is bounded by [`MAX_FRAME_BYTES`] by construction (the two are held
82/// equal on purpose so anything a backend admits to a reply, the socket can
83/// frame). Larger result sets are not returned as one oversized reply: they
84/// page via [`MAX_PAGE_SIZE`] rows plus `offset`. Raising this means raising
85/// `MAX_FRAME_BYTES` in lockstep across the server, LaserData Cloud, and the
86/// socket buffer, since it is the same frame.
87pub const MAX_QUERY_REPLY_BYTES: usize = MAX_FRAME_BYTES;
88
89/// The most managed requests one mixed-operation batch
90/// ([`AGDX_BATCH_CODE`](crate::codes::AGDX_BATCH_CODE)) may carry. Bounds the
91/// server work one frame can demand. The assembled reply is additionally
92/// bounded by [`MAX_FRAME_BYTES`] like any other.
93pub const MAX_BATCH_OPS: usize = 64;
94
95// Agent Data Exchange Protocol (AGDX) envelope caps, sized to sit inside the existing cap
96// family. The metadata caps are the load-bearing ones: that field is
97// bridge-injected, so a hostile or buggy edge gets a publish-time rejection
98// instead of inflating every record on a topic.
99/// Cap on the envelope's vocabulary strings (`operation`, `tool`,
100/// `finish_reason`), each.
101pub const MAX_AGENT_STRING_BYTES: usize = 256;
102/// Cap on a producer-supplied idempotency key.
103pub const MAX_IDEMPOTENCY_KEY_BYTES: usize = 64;
104/// Max entries in an envelope's `metadata` map.
105pub const MAX_METADATA_ENTRIES: usize = 32;
106/// Max bytes in one `metadata` key.
107pub const MAX_METADATA_KEY_BYTES: usize = 256;
108/// Max bytes in one `metadata` value (scalar/text size).
109pub const MAX_METADATA_VALUE_BYTES: usize = 1024;
110/// Max total bytes across the whole `metadata` map.
111pub const MAX_METADATA_TOTAL_BYTES: usize = 8192;
112/// Cap on a [`BodyRef`](crate::agent::BodyRef) `reference` string (a URI,
113/// object key, or KV key naming where the externalized body lives).
114pub const MAX_BODY_REFERENCE_BYTES: usize = 1024;
115/// Max capability entries on an [`AgentCard`](crate::agent::AgentCard).
116pub const MAX_CARD_CAPABILITIES: usize = 64;
117/// Ceiling on a connection's advertised metadata (`AGDX_SET_CLIENT_METADATA`).
118/// The metadata is an opaque byte payload any client may set, not agent-only: an
119/// agent advertises its card under the AGDX schema, a regular app sets whatever
120/// blob its own tooling and the console interpret. The ceiling bounds the
121/// per-connection state the streaming server holds and, because the discovery
122/// read (`AGDX_GET_CLIENTS_METADATA`) returns a page of N connections at once, it
123/// also bounds a page at `N * this`. 64 KiB is generous for any card or app blob
124/// while keeping a page well under the frame cap even at the max page size.
125pub const MAX_CLIENT_METADATA: usize = 64 * 1024;
126
127// Memory and graph caps, sized inside the existing cap family. A memory body
128// shares the opaque-value ceiling. A recall page shares the query page cap.
129/// Max bytes in one memory item's body (shares the opaque-value ceiling).
130pub const MAX_MEMORY_BODY_BYTES: usize = MAX_VALUE_BYTES;
131/// Max items a single recall returns (shares the query page cap).
132pub const MAX_RECALL_LIMIT: usize = MAX_PAGE_SIZE;
133
134/// Byte cap on a lexical search's query text (`TextQuery.query`).
135pub const MAX_TEXT_QUERY_BYTES: usize = 1024;
136/// Recall page size when the caller sets none.
137pub const DEFAULT_RECALL_LIMIT: usize = DEFAULT_STREAM_PAGE_SIZE;
138/// Max tags on one memory item.
139pub const MAX_MEMORY_TAGS: usize = 16;
140/// Max bytes in one memory tag.
141pub const MAX_MEMORY_TAG_BYTES: usize = 64;
142/// Maximum graph name length, in bytes. Bounded and control-character-free
143/// like a namespace: the name keys the projection registry and every
144/// traversal request.
145pub const MAX_GRAPH_NAME_BYTES: usize = 128;
146/// Hard ceiling on the hop depth a single graph traversal may request.
147pub const MAX_GRAPH_TRAVERSE_DEPTH: u32 = 8;
148/// Hard ceiling on nodes plus edges in one graph reply.
149pub const MAX_GRAPH_RESULT_ELEMENTS: usize = 10_000;
150/// Max labels on one graph node.
151pub const MAX_GRAPH_NODE_LABELS: usize = 16;
152/// Max encoded bytes of a node or edge `source` provenance reference. A source
153/// names a stream, topic, key, or id, all of which are bounded inputs already,
154/// so this is a defensive ceiling against a hostile or buggy upsert inflating
155/// per-element state. Sized at two key-lengths, since the largest variant (a
156/// key-value source) names both a namespace and a key, far under the opaque
157/// value ceiling, as a source ref is a short pointer, not a payload.
158pub const MAX_SOURCE_REF_BYTES: usize = 2 * MAX_KEY_BYTES;