Files
rea/src/server/environment/stateIntake.ts
T
gensokyo-dev b0fb51cb4c 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)
2026-10-28 06:48:26 +00:00

368 lines
13 KiB
TypeScript

import { fileURLToPath } from "node:url";
import { mediaTypeEssence } from "../../domain/analysisView/recordedMediaType.js";
import {
ArtifactResourceRetryFailure,
type ArtifactResourceScope,
} 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 {
RegularFileCapacityError,
readStableFileBytes,
} from "../../filesystem/readStableFileBytes.js";
import {
RegularFileCleanupFailure,
withOwnedRegularFile,
} from "../../filesystem/OwnedRegularFile.js";
/** Why a named file or download could not be read, or a call was cancelled: a category naming neither the uri nor the content. */
export type IntakeFailure =
| "not-found"
| "permission"
| "not-regular-file"
| "changed"
| "read-failed"
| "timeout"
| "http-status"
| "network"
| "cancelled";
/** A JSON-RPC error the data plane answers with; `data` locates a document problem, names an intake failure or states the response budget. */
export interface RpcError {
readonly code: number;
readonly message: string;
readonly data?:
| { readonly path: string }
| { readonly reason: IntakeFailure }
| { readonly max_response_bytes: number; readonly reply_bytes: number };
}
const INVALID_PARAMS = -32602;
/** The data plane could not complete an operation whose params were valid. */
export const SERVER_ERROR = -32000;
/** A call the server's stop or its caller's departure ended before it could change the state. */
export const CANCELLED: RpcError = {
code: SERVER_ERROR,
message: "the call was cancelled and left the state unchanged",
data: { reason: "cancelled" },
};
/** An https state download must finish within this time. */
export const STATE_FETCH_TIMEOUT_MS = 30_000;
/** Fetches one https state document; injectable so tests make no external requests. */
export type StateFetcher = (url: URL, signal: AbortSignal) => Promise<Response>;
/** Redirects are refused so a download never leaves the https origin the caller named. */
export const fetchStateDocument: StateFetcher = (url, signal) =>
fetch(url, { signal, redirect: "error" });
/** How `data/add` reaches documents a uri names; injectable so tests make no external requests and need not wait. */
export interface StateIntake {
readonly fetchState: StateFetcher;
readonly timeoutMs: number;
}
/** The intake a served environment uses. */
export const DEFAULT_STATE_INTAKE: StateIntake = {
fetchState: fetchStateDocument,
timeoutMs: STATE_FETCH_TIMEOUT_MS,
};
/** Stands in for a number that native JSON parsing or serialization would change. */
const NOT_EXACT_JSON = Symbol("not exact JSON");
/** Mark values native parsing would change, so validation refuses them rather than storing an altered value. */
export const exactJsonReviver = (
_key: string,
value: unknown,
context?: { readonly source?: string },
): unknown =>
typeof value === "number" && !isExactJsonNumber(value, context?.source)
? NOT_EXACT_JSON
: value;
export const invalidParams = (detail: string, path?: string): RpcError => ({
code: INVALID_PARAMS,
message: `invalid params: ${detail}`,
...(path === undefined ? {} : { data: { path } }),
});
const TOO_LARGE = invalidParams(
`the state document exceeds ${String(STATE_INTAKE_LIMIT_BYTES)} bytes`,
);
const unreadable = (reason: IntakeFailure): RpcError => ({
...invalidParams("the file uri does not name a readable regular file"),
data: { reason },
});
const fetchFailed = (reason: IntakeFailure): RpcError => ({
code: SERVER_ERROR,
message: "the state document could not be downloaded",
data: { reason },
});
const FILE_FAILURES: ReadonlyMap<string, IntakeFailure> = new Map([
["ENOENT", "not-found"],
["ENOTDIR", "not-found"],
["EACCES", "permission"],
["EPERM", "permission"],
["EISDIR", "not-regular-file"],
["ENOTFILE", "not-regular-file"],
["ESTALE", "changed"],
]);
/** The coarse category of a file open or read failure, from its error code. */
const fileFailure = (cause: unknown): IntakeFailure =>
(cause instanceof Error && "code" in cause
? FILE_FAILURES.get(String(cause.code))
: undefined) ?? "read-failed";
const isRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>
typeof value === "object" && value !== null && !Array.isArray(value);
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> => {
let text: string;
try {
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
} catch {
return err(invalidParams("the file is not UTF-8 text"));
}
const parsed = safeParseJson(text, exactJsonReviver);
return parsed.ok
? ok(parsed.value)
: err(invalidParams("the file is not JSON"));
};
const CLOSE_FAILED: RpcError = {
code: SERVER_ERROR,
message:
"the state file could not be closed; the server retries the close before the next file read and at shutdown",
};
const STILL_OPEN: RpcError = {
code: SERVER_ERROR,
message:
"an earlier state file is still open after its close failed, so no file was read",
};
/** Read one regular file within the intake limit; a descriptor whose close fails stays with `files`, which retries it before each later read. */
const readStateFile = async (
path: string,
files: ArtifactResourceScope,
): Promise<Result<Buffer, RpcError>> => {
try {
return ok(
await files.run(() =>
withOwnedRegularFile(
path,
{
symlinks: "follow",
retainFailedClose: (owner) =>
files.retain({
kind: "file-handle",
resource: path,
handle: owner,
}),
},
({ handle }) =>
readStableFileBytes(handle, path, STATE_INTAKE_LIMIT_BYTES, {
symlinks: "follow",
}),
),
),
);
} catch (cause: unknown) {
if (cause instanceof RegularFileCleanupFailure) return err(CLOSE_FAILED);
if (cause instanceof ArtifactResourceRetryFailure) return err(STILL_OPEN);
return err(
cause instanceof RegularFileCapacityError
? TOO_LARGE
: unreadable(fileFailure(cause)),
);
}
};
/** Settles only by rejecting when the signal aborts; the rejection is always handled. */
const abortion = (signal: AbortSignal): Promise<never> => {
const aborted = new Promise<never>((_, reject) => {
if (signal.aborted) reject(signal.reason);
else
signal.addEventListener("abort", () => reject(signal.reason), {
once: true,
});
});
aborted.catch(() => undefined);
return aborted;
};
/** What ends a download: its time limit or the caller's cancellation; `failure` names which, or a network failure when neither did. */
interface DownloadBounds {
readonly signal: AbortSignal;
readonly failure: () => IntakeFailure;
}
const downloadBounds = (
timeoutMs: number,
cancel: AbortSignal,
): DownloadBounds => {
const timeout = AbortSignal.timeout(timeoutMs);
return {
signal: AbortSignal.any([timeout, cancel]),
failure: () =>
cancel.aborted ? "cancelled" : timeout.aborted ? "timeout" : "network",
};
};
/** Read a response body within the intake limit and the download's bounds, whether or not the fetcher ties the body to the signal. */
const readCapped = async (
body: ReadableStream<Uint8Array> | null,
bounds: DownloadBounds,
): Promise<Result<Buffer, RpcError>> => {
if (body === null) return ok(Buffer.alloc(0));
const reader = body.getReader();
const aborted = abortion(bounds.signal);
const chunks: Uint8Array[] = [];
let total = 0;
try {
for (;;) {
const { done, value } = await Promise.race([reader.read(), aborted]);
if (done) return ok(Buffer.concat(chunks, total));
total += value.byteLength;
if (total > STATE_INTAKE_LIMIT_BYTES) {
await reader.cancel().catch(() => undefined);
return err(TOO_LARGE);
}
chunks.push(value);
}
} catch {
void reader.cancel().catch(() => undefined);
return err(fetchFailed(bounds.failure()));
}
};
/** Download one https document within the time and size limits, ending it when `cancel` aborts. */
const downloadState = async (
url: URL,
intake: StateIntake,
cancel: AbortSignal,
): Promise<Result<Buffer, RpcError>> => {
const bounds = downloadBounds(intake.timeoutMs, cancel);
const fetching = Promise.resolve().then(() =>
intake.fetchState(url, bounds.signal),
);
let response: Response;
try {
response = await Promise.race([fetching, abortion(bounds.signal)]);
} catch {
// A response that arrives after the download ended is released unread.
void fetching
.then(
(late) => late.body?.cancel(),
() => undefined,
)
.catch(() => undefined);
return err(fetchFailed(bounds.failure()));
}
const declared = Number(response.headers.get("content-length") ?? "0");
if (!response.ok || declared > STATE_INTAKE_LIMIT_BYTES) {
await response.body?.cancel().catch(() => undefined);
return err(
response.ok
? TOO_LARGE
: {
...fetchFailed("http-status"),
message: `the state document download answered HTTP ${String(response.status)}`,
},
);
}
return readCapped(response.body, bounds);
};
const NETWORK_SHARE = invalidParams(
"a file:// uri names a path on this host, not a network share",
);
/** A path that names a host before its first segment, as `\\server\share` or `//server/share` does. */
const UNC_PATH = /^[\\/]{2}/u;
/** The local path a `file://` uri names; another host or a UNC path would open a network share, so neither is opened. */
const localFilePath = (url: URL): Result<string, RpcError> => {
if (url.host !== "" && url.host !== "localhost") return err(NETWORK_SHARE);
if (UNC_PATH.test(url.pathname)) return err(NETWORK_SHARE);
let path: string;
try {
path = fileURLToPath(url);
} catch {
return err(invalidParams("a file:// uri names a local absolute path"));
}
return UNC_PATH.test(path) ? err(NETWORK_SHARE) : ok(path);
};
/** The bytes a file part carries inline or names by a `file://` or `https://` uri; `cancel` ends a download. */
const fileBytes = async (
file: Readonly<Record<string, unknown>>,
intake: StateIntake,
cancel: AbortSignal,
files: ArtifactResourceScope,
): Promise<Result<Uint8Array, RpcError>> => {
if (typeof file.bytes === "string" && file.uri === undefined) {
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"));
if (byteLength > STATE_INTAKE_LIMIT_BYTES) return err(TOO_LARGE);
return ok(Buffer.from(file.bytes, "base64"));
}
if (
typeof file.uri !== "string" ||
file.bytes !== undefined ||
!URL.canParse(file.uri)
)
return err(
invalidParams("a file part carries exactly one of bytes or a uri"),
);
const url = new URL(file.uri);
if (url.protocol === "https:") return downloadState(url, intake, cancel);
if (url.protocol !== "file:")
return err(invalidParams("a file uri is a file:// or https:// URL"));
const path = localFilePath(url);
return path.ok ? readStateFile(path.value, files) : path;
};
/** The state document one `data/add` part carries, parsed but not yet validated; `cancel` ends a download and `files` owns file descriptors whose close failed. */
export const partDocument = async (
part: unknown,
intake: StateIntake,
cancel: AbortSignal,
files: ArtifactResourceScope,
): Promise<Result<unknown, RpcError>> => {
if (isRecord(part) && part.kind === "data" && isRecord(part.data))
return ok(part.data);
if (!isRecord(part) || part.kind !== "file" || !isRecord(part.file))
return err(
invalidParams(
"a data/add part is a data part holding the document or a file part carrying it",
),
);
const { mimeType } = part.file;
if (
mimeType !== undefined &&
(typeof mimeType !== "string" ||
mediaTypeEssence(mimeType) !== "application/json")
)
return err(invalidParams("a file part's mimeType is application/json"));
const bytes = await fileBytes(part.file, intake, cancel, files);
return bytes.ok ? parseDocumentBytes(bytes.value) : bytes;
};