mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
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:
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")
|
||||
|
||||
@@ -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" ||
|
||||
|
||||
Reference in new issue
Block a user