fix(environment): share inline state byte admission (#7721)

Reuse shared padded-base64 facts and derive encoded admission from the existing decoded-byte budget. Refuse overflow before decoded allocation and retain padded syntax. Cover capacity, compact HTTP replies and live MCP state readback.

Fixes #7719

Assisted-by: Reimu (gensokyo-skills:reimu-incident-triage)
Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)
This commit is contained in:
gensokyo-dev authored and GitHub committed 2026-10-28 06:48:26 +00:00
1 parent 5ae77656e7
commit b0fb51cb4c
3 files changed
+101 -5

No files matched your search

@@ -879,6 +879,46 @@ const withClient = async (
}
};
it("loads a padded inline state file and keeps the RPC result compact", async () => {
const environment = await serveState({ source, state });
const document = JSON.stringify(renamedDocument()).padEnd(
2 * 1024 * 1024,
" ",
);
const added = await fetch(new URL(ENVIRONMENT_RPC_PATH, environment.url), {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "data/add",
params: {
parts: [
{
kind: "file",
file: { bytes: Buffer.from(document).toString("base64") },
},
],
},
}),
});
expect(added.status).toBe(200);
const reply = await added.text();
expect(JSON.parse(reply)).toEqual({ jsonrpc: "2.0", id: 1, result: {} });
expect(Buffer.byteLength(reply)).toBeLessThan(128);
await withClient(environment, async (client) => {
const read = await client.callTool({
name: "get_users_by_id",
arguments: { id: "2" },
});
expect(read.isError).toBeFalsy();
expect(read.structuredContent).toMatchObject({
body: { login: "renamed" },
});
});
await expect(environment.close()).resolves.toBeUndefined();
});
describe("serveEnvironment with a state", () => {
it("declares the data plane, serves read tools from the state and reflects data/add in the next answer", async () => {
const environment = await serveState({ source, state });
@@ -1,11 +1,64 @@
import { expect, it, vi } from "vitest";
import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
import { STATE_INTAKE_LIMIT_BYTES } from "../../application/environment/environmentStateDocument.js";
import {
DEFAULT_STATE_INTAKE,
fetchStateDocument,
partDocument,
STATE_FETCH_TIMEOUT_MS,
} from "./stateIntake.js";
const inlineDocument = async (bytes: string) => {
const files = new ArtifactResourceScope();
try {
return await partDocument(
{ kind: "file", file: { bytes } },
DEFAULT_STATE_INTAKE,
new AbortController().signal,
files,
);
} finally {
await files.close();
}
};
it("preserves padded syntax without imposing canonical pad bits", async () => {
await expect(inlineDocument("e31=")).resolves.toEqual({
ok: true,
value: {},
});
});
it("admits an inline JSON document at the decoded-byte budget", async () => {
const bytes = Buffer.from(
"{}".padEnd(STATE_INTAKE_LIMIT_BYTES, " "),
).toString("base64");
await expect(inlineDocument(bytes)).resolves.toEqual({ ok: true, value: {} });
});
it.each([0, 1])(
"refuses decoded-byte overflow before allocating the decoded buffer: %s extra quartets",
async (extraQuartets) => {
const bytes = "AAAA".repeat(
Math.ceil(STATE_INTAKE_LIMIT_BYTES / 3) + extraQuartets,
);
const decoded = vi.spyOn(Buffer, "from");
try {
await expect(inlineDocument(bytes)).resolves.toMatchObject({
ok: false,
error: {
code: -32602,
message: expect.stringContaining(String(STATE_INTAKE_LIMIT_BYTES)),
},
});
expect(decoded.mock.calls.length).toBe(0);
} finally {
decoded.mockRestore();
}
},
);
it("downloads with redirects refused and the 30 s timeout by default", async () => {
const fetched = vi
.spyOn(globalThis, "fetch")
+8 -5
View File
@@ -7,6 +7,7 @@ import {
} from "../../artifacts/ArtifactResourceScope.js";
import { STATE_INTAKE_LIMIT_BYTES } from "../../application/environment/environmentStateDocument.js";
import { isExactJsonNumber } from "../../domain/exactJsonNumber.js";
import { paddedBase64ByteLength } from "../../domain/paddedBase64.js";
import { err, ok, type Result } from "../../domain/result.js";
import { safeParseJson } from "../../domain/safeJson.js";
import {
@@ -126,8 +127,8 @@ const fileFailure = (cause: unknown): IntakeFailure =>
const isRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>
typeof value === "object" && value !== null && !Array.isArray(value);
const BASE64 =
/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u;
const MAX_INLINE_BASE64_CHARACTERS =
Math.ceil(STATE_INTAKE_LIMIT_BYTES / 3) * 4;
/** Parse loaded bytes as UTF-8 JSON, marking values parsing would change; the content is never echoed. */
const parseDocumentBytes = (bytes: Uint8Array): Result<unknown, RpcError> => {
@@ -316,10 +317,12 @@ const fileBytes = async (
files: ArtifactResourceScope,
): Promise<Result<Uint8Array, RpcError>> => {
if (typeof file.bytes === "string" && file.uri === undefined) {
if (!BASE64.test(file.bytes))
if (file.bytes.length > MAX_INLINE_BASE64_CHARACTERS) return err(TOO_LARGE);
const byteLength = paddedBase64ByteLength(file.bytes);
if (byteLength === undefined)
return err(invalidParams("the file part's bytes are not base64"));
const bytes = Buffer.from(file.bytes, "base64");
return bytes.length > STATE_INTAKE_LIMIT_BYTES ? err(TOO_LARGE) : ok(bytes);
if (byteLength > STATE_INTAKE_LIMIT_BYTES) return err(TOO_LARGE);
return ok(Buffer.from(file.bytes, "base64"));
}
if (
typeof file.uri !== "string" ||