Files
reverse-engineering-anything/src/sqlite/SqliteDatabaseWorkerExecution.ts
T
morlutoandCursor Agent ef59174c1e fix(errors): name fixed allowances in resource-constraint recovery (#5633)
* fix(errors): name fixed allowances in resource-constraint recovery

Fixed parser, count, report, input, and capture refusals inherited
host-memory, wait-and-retry, or retained-Evidence advice. Producers now
attach allowance-specific recovery, and deterministic capacity caps are
not retryable. IL2CPP reader occupancy stays retryable capacity.

Refs #5556, #5405, #5456, #5459, #5460, #5504, #5505, #5506, #5537, #5628, #5630, #5631, #5632

* fix(il2cpp): keep a closing metadata reader non-retryable

close() stops admission before the active inspection finishes. A second
inspect in that window was projected as retryable capacity. Closing and
closed readers now report capability_unavailable. Occupied readers that
remain open stay retryable.

* fix(errors): align fixed-cap recovery with retained facts

IL2CPP report refusals keep observed source facts instead of denying Evidence. Command-output caps use the capture allowance. Ghidra seed recovery names the limit that was exceeded.

* fix(errors): call byte caps a byte allowance

WASM representation limits and the IL2CPP Ghidra seed-file cap are byte budgets. Their recovery no longer says the exceeded allowance is a fixed count.

* test(ida): keep unresolved procedure lookups non-retryable

Main now treats an unchanged invalid request as non-retryable. The IDA lookup tests follow that projection.

* fix(errors): qualify capture recovery for live output

A fixed capture allowance still rejects the same output. Live captures such as adb logcat can change between requests, so recovery no longer says every unchanged request fails again.

* fix(adb): keep a truncated live capture retryable

Device output can shrink between calls, so an adb capture that exceeds the allowance stays retryable. Deterministic capture and version-output limits stay non-retryable.

* fix(adb): keep truncated mutations non-retryable

Only live observations opt into retrying a capture that exceeded the allowance. Push, install, uninstall, and other mutations stay non-retryable.

* fix(errors): keep live capture recovery off deterministic limits

Artifact-derived capture allowances again say the unchanged request fails. Only volatile adb observations use the live-output sentence.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-10-21 03:45:34 +08:00

216 lines
6.8 KiB
TypeScript

import {
bindArtifactFileCleanup,
ArtifactFileCleanupFailure,
artifactFileCancellationCause,
} from "../artifacts/ArtifactFileOperation.js";
import { randomUUID } from "node:crypto";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { z } from "zod";
import { fixedAllowanceOptions } from "../domain/fixedAllowanceRemediation.js";
import type { AnalysisCapturedOutput } from "../domain/analysisErrorBase.js";
import {
AnalysisCancelledError,
AnalysisInputError,
AnalysisCapabilityUnavailableError,
AnalysisOutputError,
AnalysisResourceConstraintError,
} from "../domain/analysisErrorCore.js";
import {
sqliteDatabaseSchema,
type InspectSqliteDatabaseInput,
} from "../domain/sqlite/sqliteDatabase.js";
import { readStableArtifact } from "../artifacts/readStableArtifact.js";
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import type { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
import { runOwnedCommand } from "../process/OwnedCommand.js";
import type {
ProviderProcessSupervisor,
ProviderProcessStopResult,
} from "../process/ProviderProcess.js";
import { capturedSqliteOutput } from "./SqliteDatabaseFailures.js";
import { SQLITE_DATABASE_LIMITS } from "./SqliteDatabaseLimits.js";
const operation = "inspect_sqlite_database";
const {
artifact: _artifactSchema,
wal: _walSchema,
...inspectionShape
} = sqliteDatabaseSchema.shape;
const inspectionSchema = z.strictObject(inspectionShape);
const replySchema = z.discriminatedUnion("ok", [
z.strictObject({ ok: z.literal(true), inspection: inspectionSchema }),
z.strictObject({
ok: z.literal(false),
reason: z.enum([
"format",
"selection",
"column-selection",
"unsupported",
"unavailable",
"output-limit",
"memory",
]),
message: z.string(),
}),
]);
/** Replaceable launcher retains the shared owned-process supervision boundary. */
export type SqliteWorkerLauncher = NonNullable<
Parameters<typeof runOwnedCommand>[2]
>["launcher"];
// Each input rejection names the caller argument that requires correction.
const rejectedArgument = {
format: "path",
unsupported: "path",
selection: "table",
"column-selection": "columns",
} as const;
const rejectedReply = (
reply: z.output<typeof replySchema> & { ok: false },
capturedOutput: AnalysisCapturedOutput,
) => {
if (reply.reason === "unavailable")
return new AnalysisCapabilityUnavailableError(
"sqlite",
operation,
reply.message,
{ capturedOutput, userMessage: reply.message },
);
if (reply.reason === "memory")
return new AnalysisResourceConstraintError(
operation,
"memory",
reply.message,
{
javascript_heap_bytes: 256 * 1024 * 1024,
},
{ capturedOutput },
);
if (reply.reason === "output-limit")
return new AnalysisResourceConstraintError(
operation,
"transport",
reply.message,
{
sqlite_value_or_record_bytes: SQLITE_DATABASE_LIMITS.outputBytes,
worker_reply_bytes: SQLITE_DATABASE_LIMITS.outputBytes,
},
{
capturedOutput,
...fixedAllowanceOptions("The SQLite worker reply allowance", "report"),
},
);
return new AnalysisInputError(operation, { capturedOutput }, [
{
path: [rejectedArgument[reply.reason]],
reason: "invalid_format",
message: reply.message,
},
]);
};
/** Execute only the private snapshot and validate the bounded worker protocol. */
export const executeSqliteDatabaseWorker = async (request: {
readonly input: InspectSqliteDatabaseInput;
readonly root: string;
readonly snapshotPath: string;
readonly environment: Readonly<NodeJS.ProcessEnv>;
readonly resourceScope: ArtifactResourceScope;
readonly launcher?: SqliteWorkerLauncher;
readonly signal?: AbortSignal;
readonly captured: (output: AnalysisCapturedOutput) => void;
readonly retainCleanup?: (
owner: ProviderProcessSupervisor,
result: Extract<ProviderProcessStopResult, { status: "incomplete" }>,
) => void;
}): Promise<z.output<typeof inspectionSchema>> => {
const requestPath = join(request.root, "request.json");
const replyPath = join(request.root, "reply.json");
await writeFile(
requestPath,
JSON.stringify({
snapshot_path: request.snapshotPath,
reply_path: replyPath,
input: request.input,
}),
{ flag: "wx", mode: 0o600 },
);
const { NODE_OPTIONS: _ambientNodeOptions, ...environment } =
request.environment;
const execution = await runOwnedCommand(
{
command: process.execPath,
arguments: [
"--max-old-space-size=256",
fileURLToPath(new URL("./SqliteDatabaseWorker.js", import.meta.url)),
requestPath,
],
cwd: request.root,
expectedCommand: null,
runId: `rea-sqlite-database-${randomUUID()}`,
hostEnvironment: environment,
},
{
timeoutMs: SQLITE_DATABASE_LIMITS.timeoutMs,
diagnosticBytes: SQLITE_DATABASE_LIMITS.diagnosticBytes,
},
{
...(request.signal === undefined ? {} : { signal: request.signal }),
...(request.launcher === undefined ? {} : { launcher: request.launcher }),
...(request.retainCleanup === undefined
? {}
: { retainCleanup: request.retainCleanup }),
},
);
const capturedOutput = capturedSqliteOutput(execution);
request.captured(capturedOutput);
let reply: z.output<typeof replySchema>;
try {
const file = await readStableArtifact(
replyPath,
SQLITE_DATABASE_LIMITS.outputBytes,
request.signal,
undefined,
request.resourceScope,
);
reply = replySchema.parse(JSON.parse(file.bytes.toString("utf8")));
} catch (cause: unknown) {
const options = {
cause,
capturedOutput,
...(cause instanceof ArtifactReaderFailure && cause.cleanup !== undefined
? { cleanup: cause.cleanup }
: {}),
};
if (
cause instanceof ArtifactReaderFailure &&
(cause.reason === "cancelled" ||
(cause instanceof ArtifactFileCleanupFailure &&
cause.fileCleanupFailure.outcome.kind === "completed" &&
request.signal?.aborted))
) {
const error = new AnalysisCancelledError(operation, {
...options,
cause:
cause.reason === "cancelled"
? artifactFileCancellationCause(cause)
: request.signal?.reason,
});
bindArtifactFileCleanup(cause, error);
throw error;
}
const error = new AnalysisOutputError(
operation,
`Owned SQLite worker reply is missing or malformed: ${cause instanceof Error ? cause.message : String(cause)}`,
options,
);
bindArtifactFileCleanup(cause, error);
throw error;
}
if (!reply.ok) throw rejectedReply(reply, capturedOutput);
return reply.inspection;
};