fix(environment): keep bodies too deep for the state's nesting out of the state

A recorded body at the JSON depth limit was accepted by extraction, then
nested a few levels deeper as a document or entity value, so sealing the
state Evidence threw and environment-serve and environment-export stopped
instead of falling back. stateBodyJson, the one rule extraction and
serving share, now treats a body nesting deeper than the limit minus 16
levels as unusable, so its tool stays a recorded view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Edgar ArakelyanandClaude Opus 5.5 committed 2026-10-20 00:15:39 -07:00
1 parent 83fcc739f7
commit d9c0357b2a
5 files changed
+81 -2

No files matched your search

@@ -61,3 +61,12 @@ any path conversion on every platform), a real `https://` download (tests
inject the fetcher), the memory and time of a budgeted batch (not measured),
agent token usage, a real AgentEnv framework run, a real-world capture, and
write effects, which are a later increment.
After refusing bodies too deep for the state's own nesting (a recorded body
must nest at most the JSON depth limit minus 16 levels to enter the state):
`npm run check:fast`, `npm run format:check`, `npx knip`, `npm run scan:todos`
passed; `npm run jscpd`: 3.37% over 3373 files; `npx vitest run --project
domain --project services`: 347 files, 5950 tests passed, 5 pre-existing
skips; `npm run build:cached`, then `npm run test:focused --` the serve CLI,
MCP and process boundaries, the export CLI boundary and the two serve adapter
tests: 6 files, 48 tests passed.
@@ -17,6 +17,7 @@ import {
} from "../../../tests/fixtures/environmentServe.js";
import { AnalysisInputError } from "../../domain/analysisErrorCore.js";
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
import type { JsonValue } from "../../domain/jsonValue.js";
import {
inspectAnalysisView,
inspectAnalysisViewValidated,
@@ -226,6 +227,28 @@ describe("prepareServedState", () => {
});
});
it("derives a state when a body leaves no room for the state's own nesting", () => {
let deep: JsonValue = { theme: "dark" };
for (let level = 0; level < 995; level += 1) deep = { nested: deep };
const deepFixture = environmentStateServeFixture(
timedCaptureEvidence([
["GET", "/api/settings", deep],
["PUT", "/api/settings", { theme: "light" }],
]),
);
const deepSource = loadEnvironmentSource(
deepFixture.manifest,
deepFixture.capture,
);
if (!deepSource.ok) throw deepSource.error;
expect(
prepareServedState(deepSource.value, deepFixture.capture).report,
).toEqual({
served: true,
evidence_id: deepFixture.state.evidence_id,
});
});
it("serves replay and reports the code and message when the capture yields a manifest but no state", () => {
const headerOnly = scenarioServeFixture(
scenarioHeaderOnlyCaptureEvidence(),
@@ -1,7 +1,7 @@
import { z } from "zod";
import type { AnalysisError } from "../analysisErrorBase.js";
import { parseExactJson } from "../exactJsonNumber.js";
import type { JsonValue } from "../jsonValue.js";
import { MAX_JSON_DEPTH, type JsonValue } from "../jsonValue.js";
import { ok, type Result } from "../result.js";
import {
WEB_NETWORK_CAPTURE_LIMITS,
@@ -15,6 +15,7 @@ import {
import { EnvironmentStateExtraction } from "./EnvironmentStateExtraction.js";
import type { TemplateRule } from "./environmentManifestRules.js";
import {
deeperThan,
hasPrototypeKey,
isStateWrite,
type ParsedBody,
@@ -52,6 +53,13 @@ type StateFields = Pick<
/** Bodies beyond this many UTF-8 bytes are not parsed into state; their views stay recorded. */
export const STATE_BODY_LIMIT_BYTES = 256 * 1024;
/**
* Levels the state's own nesting adds above a recorded value: Evidence, a `data/get` reply and a
* JSON-RPC response hold a body's members about ten levels down, so a body must leave this much
* room under the JSON depth limit to be sealed and served.
*/
const STATE_NESTING_DEPTH = 16;
/** A recorded response body as state JSON: a JSON-like media type, complete UTF-8 text within the state body limit, exact numbers and no `__proto__` member; otherwise undefined. */
export const stateBodyJson = (
contentType: string | null,
@@ -67,7 +75,11 @@ export const stateBodyJson = (
)
return undefined;
const value = parseExactJson(body.bodyText);
return value === undefined || hasPrototypeKey(value) ? undefined : value;
return value === undefined ||
hasPrototypeKey(value) ||
deeperThan(value, MAX_JSON_DEPTH - STATE_NESTING_DEPTH)
? undefined
: value;
};
const timingSchema = z.object({
@@ -297,6 +297,24 @@ export const jsonBytes = (value: JsonValue): number => {
return ENTRY_BYTES;
};
/** Whether a JSON value nests more than `limit` levels below its root. */
export const deeperThan = (value: JsonValue, limit: number): boolean => {
const pending: { readonly value: JsonValue; readonly depth: number }[] = [
{ value, depth: 0 },
];
for (let next = pending.pop(); next !== undefined; next = pending.pop()) {
if (next.depth > limit) return true;
const children = Array.isArray(next.value)
? next.value
: isObject(next.value)
? Object.values(next.value)
: [];
for (const child of children)
pending.push({ value: child, depth: next.depth + 1 });
}
return false;
};
/** JSON parsers keep a `__proto__` member that plain object construction would turn into a prototype. */
export const hasPrototypeKey = (value: JsonValue): boolean =>
Array.isArray(value)
@@ -327,6 +327,23 @@ it("leaves recorded the views it cannot reproduce, with the reason", () => {
expect(state.entities).toEqual([]);
});
it("leaves a body too deep for the state's own nesting recorded instead of failing to seal", () => {
let deep: JsonValue = { theme: "dark" };
for (let level = 0; level < 995; level += 1) deep = { nested: deep };
const state = stateOf(
captureOf([
["GET", "/api/settings", deep],
["PUT", "/api/settings", { theme: "light" }],
]),
);
expect(state.documents).toEqual([]);
expect(viewOf(state, "get_settings")).toEqual({
tool: "get_settings",
shape: "recorded",
reason: "body-unusable",
});
});
it("holds a written path's non-entity read as a document", () => {
const state = stateOf(
captureOf([