Files
rea/docs/specs/agent-environments/state-serve/spec.md
T
Cursor Agent cfb42ddde0 feat(environment): apply evidence-gated write effects to the served state
Serve write tools whose later reads confirm the effect, and keep main's
state-file ownership and part-wise reply measurement.

Squashed rebase of #5600 onto the rebased #5352 tip.
2026-10-21 22:12:30 +00:00

192 lines
13 KiB
Markdown

# Serve the environment from its state by default
Issue: https://github.com/morluto/rea/issues/4933
## Outcome
`rea environment-serve <manifest> --evidence <capture>` serves the environment
from the initial state the capture observed, with nothing else to supply: at
startup it derives the `environment-state` view of the capture in-process with
the manifest's `template_rules`. Read tools whose views are state-backed answer
from one live state, the `/agentenv` data plane implements `data/reset`,
`data/add` and `data/get` against it, and the card advertises those operations
and an intake declaration. `--replay-only` serves exactly the stateless replay
main serves: every manifest tool replays its recordings, `/agentenv` answers every method as not found and the card is
byte-identical. A state that cannot be derived, or whose catalog exceeds the
discovery budget, starts the server in replay mode, logs why and reports it. Exported bundles serve the state by default,
and their verifier and README match what they serve. Serving is read-only in
this increment: write tools are not served from the state.
## Requirements
- Derive the state in the application layer, shared by serve and export:
`deriveEnvironmentState(source, capture, operation)` authenticates the
capture value the command already read (an owned snapshot authenticated by
the source load is reused, not parsed again), projects the
`environment-state` view with the manifest's `template_rules` through
`inspectAnalysisViewValidated` with the capture as retained Evidence (the
Evidence id equals that of a projection by hand, and the capture's strings
are not charged again as inline input, which refuses captures of a few MiB),
then applies `loadEnvironmentState`: `inspect_analysis_view` Evidence of kind
`environment-state`, its `parent_evidence_id` equal to the capture's, its
`template_rules` equal to the manifest's in the same order, and every view
naming a manifest tool, refusing otherwise with `invalid_request` whose issue
path starts with `state`. The source validates a HAR capture with the owned
borrowing parse, so the view reuses that validation.
- `prepareServedState(source, capture, {name, operation})` first builds the
state catalog under the served name, then derives the state, and reports
`{served: true, evidence_id}` or
`{served: false, reason: "unavailable", code, message}` with the refusal's
projected code and message: a state catalog over the discovery budget is
unavailable with `resource_constraint`, and no state is derived for it. A
derived state whose catalog fits always serves from the state. The CLI
reports `{served: false, reason: "replay-only"}` itself for `--replay-only`
and derives nothing.
- Keep one state store per server process, shared by every MCP session and the
data plane: entities keyed by type, collection and id with per-collection
order taken from the entities array, documents keyed by tool. `reset`
restores the Evidence state, `replace` installs a validated document whole,
and mutations are synchronous.
- Answer item views by rendering the entity the path arguments name into the
recorded envelope of `envelope_ordinal`. Match arguments as replay does: a
held collection segment equal to the argument as written or after
percent-decoding, preferring verbatim spellings, and the identity as the last
argument percent-decoded, then as written, never re-split on `/`. Any held
entity answers, and query arguments only set `replay.exact_match`. Refuse an
entity the state does not hold, or one lacking a required field, as a tool
error and never invent a body.
- Answer list views from the state only when the arguments name one of the
view's `checked_record_ordinals` exactly (every path and query value equal and
no recorded query parameter unsupplied), choosing among them as replay
chooses: every collection entity in state order for `membership: "all"`,
otherwise the recorded members the state still holds. A member lacking one
of the view's `required_fields` makes the call a tool error stating that the
state holds no complete member, never a partial body, as for items. Lines
recorded after a write or with other filters were never checked and answer
from recordings.
- Answer document views with the state's value when the arguments name the
document's checked line exactly.
- Answer recorded views, tools without a view and unrecorded requests from the
recording, labelled `state: {source: "recording", reason}`; label state
answers `state: {source: "state", view, …}` with the base record's replay
provenance and `truncated: false`; refuse a state answer over 256 KiB as a
tool error rather than cut it.
- Serve `data/reset` (`{}`), `data/add` with exactly one `data` or `file` part
(`bytes`, a `file://` regular file or an `https://` download with a 30 s
timeout covering the body, no redirects and a streamed 32 MiB cap;
`mimeType` when given is `application/json`) and `data/get` (`{parts:
[{kind: "data", data: rea_state}]}`). A `file://` uri whose host is neither
empty nor `localhost`, or whose path is a UNC path, is `-32602` before
anything is opened. A document that `data/get` would write back past 32 MiB,
once it adds the members a document may omit, is `-32602`, so a reported
state always loads again. Anything else is `-32602`, including empty `parts`; a
failed download is `-32000`; errors never echo a uri or content and name a
coarse failure category in `error.data.reason`. Calls run one at a time in
arrival order across requests; every message of a batch is queued when the
batch arrives, so no other request's call lands between them, and
notifications run without a reply.
- Stopping the server (`close` or `prepareShutdown`) aborts the data plane's
stop signal, and a request's own signal aborts when its client leaves: the
download in progress ends with reason `cancelled`, queued calls are answered
`-32000` with reason `cancelled` without running, and a `data/add` whose
file read finished after the cancellation does not replace the state.
- Bound one `/agentenv` response to 64 MiB of replies (twice the intake
limit). Each reply is measured in bounded JSON parts as it is produced, in
message order, and assembled only when it fits, so a reply past the budget is
counted but never serialized whole. Room is kept for every later reply's
budget error and for a batch's brackets and commas, so the body never
passes the budget; a reply that does not fit is replaced by `-32000` with
`data: {max_response_bytes, reply_bytes}`, unless that error is no smaller
than the reply, so the body is never built past the budget and small write
replies are kept.
- Validate `rea-state/v1` documents against the served types and views: known
types, collections that instantiate their template, ids equal to the identity
field, unique keys, `view_fields` naming item or list views of the type
without setting the type's identity field to a value whose scalar text
differs from the entity's id (refused at that view's `fields`),
documents naming document views once at the initial path, and the optional
`state_evidence_id` and `types` that `data/get` reports equal to the served
ones. Refuse numbers without an exact native value and `__proto__` members.
Name the first problem by JSON pointer, never deeper than a caller-data
member (`fields`, `value`, `types`), and leave the state unchanged.
- Keep record citations Evidence provenance: refuse `record_ordinals` or a
`record_ordinal` other than the Evidence's for that entity or document, and
hold the Evidence's citations only for loaded values equal to the Evidence's;
edited or added values cite none.
- In state mode, declare `operations: ["data/reset", "data/add", "data/get"]`
and the `urn:agentenv:intake/v1` extension, describe tools as answering from
the state where it backs them, and leave out `POST`, `PUT`, `PATCH` and
`DELETE` tools other than query `POST`s. The discovery budget reserves the
card and descriptions actually served (`servedCatalog` returns the tools and
the data plane the card declares). A state catalog can exceed the budget a
replay catalog fits, so such a state is unavailable and the environment and
its export serve replay: everything main served and exported is still
served and exported.
- The CLI has no `--state`; `--replay-only` (default false) serves replay
without deriving. Otherwise it prepares the state under the name it serves
(`ENVIRONMENT_NAME` or the manifest's). When the state is unavailable the
command logs a warning with the code and message through its listener logger
before binding, and serves replay. The stopped document's `state` member is
the report. `serveEnvironment` keeps taking an optional prepared state and
serves replay without one; the served catalog (`servedCatalog`) and the
answer function (`environmentToolAnswer`) are shared with export.
- `environment-export` keeps the bundle command
`rea environment-serve /env/manifest.json --evidence /env/evidence.json` and,
after its name checks, prepares the state under the bundle name as that
server will, to predict what it serves, so a refused `--name` derives
nothing; its report is served or unavailable, never replay-only. The
verifier's `TOOL_COUNT` and the command's `tool_count` are the served tool
count, and its check tool and expected status come from calling the server's
answer function with `{}` on the first served `GET` tool without path
parameters whose answer is not an error. The verifier and smoke task use the
id `serve`. The README's "What the environment serves" section describes
serving from the state, the replay fallback and `--replay-only`, says which
applies to the bundle (with the code and message when the state is
unavailable), and keeps the not-emulation and untrusted-content caveats. The
export output adds `state`. The hosted-lane verifier script expects the
served count.
## Acceptance and limits
Services tests cover derivation (the by-hand Evidence id and state, a capture
the manifest was not projected from, a capture inline admission refuses),
the prepared reports (served, unavailable for a `capture_browser_scenario`
capture with header values but no response bodies, unavailable with
`resource_constraint` for a 350-tool read-only manifest whose replay catalog
fits), bundle plans served from the state and in replay (tool count, check tool
and status, a check whose state answer differs from replay's in record and
status, a check that skips a tool the server cannot answer, the replay fallback
of that 350-tool manifest, name refusals before any preparation, a manifest
neither mode admits, verifier, smoke task and README wording), the loader
refusals, store reset, order, round trip and
atomic replace, the first-problem pointers, citation provenance after a load and
every answer path: item (including percent-encoded collection segments and
ids), list `all`, list `recorded`, lines recorded after a write or with other
filters, a line replay's tie-break would not pick, document, recorded view, tool
without a view, unrecorded request, missing entity, missing required field and
the body limit, a view value that changes an entity's identity, and a list
member missing a required field. Adapters tests cover the data-plane shapes,
`-32602` for empty and malformed parts and invalid documents, inexact numbers,
bytes, `file://` and its failure categories, `file://` uris naming another
host or a UNC path, injected https downloads (status, declared and streamed
size, the timeout whether or not the fetcher honours its signal), the default
fetch's refused redirects, arrival-order application across requests, a batch
another request's reset cannot split, cancellation by the server's stop and by
a departed caller, a server that closes while a download is stalled, the
response budget, batches and notifications, the card declaration, absent write
tools, a served query `POST`, the per-mode discovery reservation and a
`data/add` that changes the next tool answer over MCP. The mcp-boundary tests serve the state the
built CLI derives from only a manifest and a capture, and `--replay-only` with
every manifest tool, `-32601` data-plane answers and a card whose SHA-256
equals that of the card main serves for the fixture; the process-boundary test checks the stopped document's
`state` for a derived state, `--replay-only` and an unavailable state, and the
warning logged before binding; the cli-boundary export test checks the served
tool count, the verifier and the README of an exported bundle. No test reaches
an external network. Write effects, list filters beyond recorded lines and
`tables` in the intake declaration are out of scope.
## Deferred issue criteria
Write effects are a later increment of #4933. This PR must reference rather
than close the issue.