fix(process): name the Swift compile deadline (#6071)

Name REA_SWIFTC_COMPILE_TIMEOUT_MS, its effective milliseconds, the
1..2147483647 range, and the omitted 120000 ms default when a Darwin
ownership-helper compile times out. A change applies after REA restarts,
on the next ownership-helper compile.

Producer SIGXCPU and SIGXFSZ recovery says REA retains the tighter
inherited limit. Agent-visible fixed caps in this theme say this release
has no runtime override. Numbers are unchanged.

Refs #5910

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: morluto <morluto@users.noreply.github.com>
This commit is contained in:
morluto
2026-10-21 23:46:29 +08:00
committed by GitHub
parent f86206535f
commit d8a9673fc4
26 changed files with 321 additions and 95 deletions
+4 -2
View File
@@ -1189,8 +1189,10 @@ truncation markers. Darwin ownership-helper compilation failures also retain
the compiler outcome, exit code or signal, and bounded stderr; a timed-out
compiler remains distinguishable from a compiler that exited with an error.
The helper compile deadline defaults to 120,000 ms; set
`REA_SWIFTC_COMPILE_TIMEOUT_MS` to an integer between 1 and 2,147,483,647 when
a busy host needs longer.
`REA_SWIFTC_COMPILE_TIMEOUT_MS` to an integer between 1 and 2,147,483,647 in
the environment that starts REA when a busy host needs longer. Restart REA to
apply a change; the next ownership-helper compile uses the new value. A
timeout names that effective deadline.
On macOS, the Java prerequisite probe waits for this independently bounded
preparation before starting its five-second command deadline. Caller cancellation
still interrupts preparation, and failed helper cleanup remains owned for retry.
+1 -1
View File
@@ -68,7 +68,7 @@ The background export belongs to the session. `close_binary`, or the end of a CL
The assembly label index is scanned once per export reader. Cancelling a request stops its wait without interrupting other readers; closing the session aborts and drains the shared scan before releasing its files.
Regex procedure and string search uses ECMAScript Unicode matching in a cancellable worker with a five-second matching deadline. Literal matching remains the default. Cancellation or a matching deadline leaves the session available.
Regex procedure and string search uses ECMAScript Unicode matching in a cancellable worker with a five-second matching deadline and a 128 MiB worker heap. Literal matching remains the default. Cancellation or a matching deadline leaves the session available. This release has no runtime override for that deadline or heap.
Single-function subprocess output and each cached source facet share a 16 MiB buffered-text capacity. Cached spans are checked before allocation; larger text remains in the local export and the error identifies the file and range. `analyze_function` initially returns the complete dossier, which can still be large; later retained views do not remove that first transfer. Prefer the individual text tools for a focused question.
+7 -7
View File
@@ -807,13 +807,13 @@ auto-analysis, bridge connection, and health readiness before returning analysis
These deadlines have different owners:
| Deadline | Owner and effect |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| MCP initialize | The client bounds transport/REA connection startup, before any target query. |
| Hopper startup | REA allows 120,000 ms by default; `REA_HOPPER_STARTUP_TIMEOUT_MS` accepts decimal integers from 1–2,147,483,647 ms. Invalid values fail configuration validation. Startup failure is reported by the first provider query. |
| Ghidra startup | REA allows 330,000 ms by default; `REA_GHIDRA_STARTUP_TIMEOUT_MS` accepts decimal integers from 1–2,147,483,647 ms. Invalid supplied values fail configuration validation. Startup failure is reported by the first provider query. |
| Darwin swiftc helper | REA allows 120,000 ms by default for ownership-helper compilation; `REA_SWIFTC_COMPILE_TIMEOUT_MS` accepts decimal integers from 1–2,147,483,647 ms. Invalid values fail configuration validation. |
| Individual tool call | The client bounds its wait, including cold engine startup. The pinned client SDK 2.3.1 defaults to 60,000 ms and can cancel earlier than REA's startup deadline. |
| Deadline | Owner and effect |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| MCP initialize | The client bounds transport/REA connection startup, before any target query. |
| Hopper startup | REA allows 120,000 ms by default; `REA_HOPPER_STARTUP_TIMEOUT_MS` accepts decimal integers from 1–2,147,483,647 ms. Invalid values fail configuration validation. Startup failure is reported by the first provider query. |
| Ghidra startup | REA allows 330,000 ms by default; `REA_GHIDRA_STARTUP_TIMEOUT_MS` accepts decimal integers from 1–2,147,483,647 ms. Invalid supplied values fail configuration validation. Startup failure is reported by the first provider query. |
| Darwin swiftc helper | REA allows 120,000 ms by default for ownership-helper compilation; `REA_SWIFTC_COMPILE_TIMEOUT_MS` accepts decimal integers from 1–2,147,483,647 ms. Invalid values fail configuration validation. Set it in the environment that starts REA and restart REA; the next ownership-helper compile uses the new value. |
| Individual tool call | The client bounds its wait, including cold engine startup. The pinned client SDK 2.3.1 defaults to 60,000 ms and can cancel earlier than REA's startup deadline. |
For [Codex and Kimi registrations](client-configuration.md), setup supplies
missing tool deadlines from the known provider allowance plus 120 seconds of
+4 -2
View File
@@ -202,8 +202,10 @@ Function dossiers retain this same string evidence. Native call edges retain
Hopper's partial `CallReference` classification and exact endpoints; detailed
reference flags remain unavailable rather than being invented.
Regex searches use ECMAScript Unicode syntax in a cancellable worker with a
five-second matching deadline. Deadline or cancellation stops matching while
leaving the Hopper API available. Literal mode retains Unicode casefold matching.
five-second matching deadline and a 128 MiB worker heap. Deadline or
cancellation stops matching while leaving the Hopper API available. This
release has no runtime override for that deadline or heap. Literal mode
retains Unicode casefold matching.
## Bounded static traces
+9
View File
@@ -545,6 +545,15 @@ missing or relative SDK/compiler path fails with an actionable prerequisite
error before compilation. An incompatible installation reports the compiler's
bounded diagnostics; REA does not repair or replace the toolchain.
The compile deadline is `REA_SWIFTC_COMPILE_TIMEOUT_MS`. An omitted value is
120,000 ms, and accepted values are integers from 1 to 2,147,483,647. REA reads
the variable from the environment that starts the process and keeps that
deadline for every helper compile in that process. Restart REA to apply a
change; the next ownership-helper compile uses the new value. A timeout names
the effective milliseconds. Invalid values fail configuration validation at
startup. Embedded compiler diagnostics are cut at 512 bytes, and this release
has no runtime override for that excerpt.
For example, select paths from your existing Xcode or Command Line Tools
installation **before** entering an enforced write boundary:
+7 -4
View File
@@ -45,7 +45,10 @@ import {
AnalysisResourceConstraintError,
} from "../domain/analysisErrorCore.js";
import { createEvidence } from "../domain/evidence.js";
import { fixedAllowanceOptions } from "../domain/fixedAllowanceRemediation.js";
import {
fixedAllowanceOptions,
fixedProducerInputAllowance,
} from "../domain/fixedAllowanceRemediation.js";
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
import {
type InspectPacketCaptureInput,
@@ -387,7 +390,7 @@ export class TsharkCaptureProvider implements PacketCapturePort {
"Packet limits count input packets before display filtering. Filtered results do not establish whole-capture or device traffic absence. Two-pass reassembly cannot recover bytes missing from the recording or beyond the read limit.",
"Repeated field values remain ordered string arrays; raw packet bytes are included only when explicitly selected as a supported field. Export index/score metadata are omitted. All stderr, including warnings on exit zero, is retained.",
"A private empty personal configuration is used. Installed libraries, global configuration and dissector plugins are trusted and not fully fingerprinted. File-only analysis and disabled name resolution do not constitute an OS sandbox.",
"Linux prlimit enforces soft and hard ceilings before execution, preserving stricter inherited limits: at most 1 GiB address space per process, 64 MiB per file, 30 CPU seconds and no core dumps. Effective limits are reported. Resident input is capped at 32 MiB, each command at 30 wall-clock seconds and combined output at 4 MiB. Output overflow fails. These are per-process/per-file limits, not aggregate memory or filesystem quotas; caller-supplied tools and the limiter are trusted.",
"Linux prlimit enforces soft and hard ceilings before execution, preserving stricter inherited limits: at most 1 GiB address space per process, 64 MiB per file, 30 CPU seconds and no core dumps. Effective limits are reported. Resident input is capped at 32 MiB, each command at 30 wall-clock seconds and combined output at 4 MiB. Output overflow fails. These are per-process/per-file limits, not aggregate memory or filesystem quotas; caller-supplied tools and the limiter are trusted. REA retains the tighter inherited limit. This release has no runtime override.",
],
}),
);
@@ -568,9 +571,9 @@ export class TsharkCaptureProvider implements PacketCapturePort {
{ input_bytes: LIMITS.input_bytes },
{
cause,
...fixedAllowanceOptions(
...fixedProducerInputAllowance(
LIMITS.input_bytes,
"the fixed packet-capture input allowance",
"input",
),
},
);
+6 -3
View File
@@ -10,12 +10,15 @@ export const DEFAULT_HOPPER_STARTUP_TIMEOUT_MS = 120_000;
/** Fresh Swift SDK cache took ~88s on the supported Intel macOS VM. */
export const DEFAULT_SWIFTC_COMPILE_TIMEOUT_MS = 120_000;
// Node schedules delays above this signed 32-bit boundary as a 1 ms timer.
const MAX_TIMER_DELAY_MS = 2_147_483_647;
/**
* Largest delay Node schedules exactly. Delays above this signed 32-bit
* boundary become a 1 ms timer, so startup and swiftc timeouts stop here.
*/
export const MAX_PROVIDER_STARTUP_TIMEOUT_MS = 2_147_483_647;
/** Parse the optional setting without reading the ambient process environment. */
export const providerStartupTimeoutSchema = z
.string()
.regex(/^[1-9]\d*$/u, "must be a positive decimal integer")
.transform(Number)
.pipe(z.number().int().min(1).max(MAX_TIMER_DELAY_MS));
.pipe(z.number().int().min(1).max(MAX_PROVIDER_STARTUP_TIMEOUT_MS));
+2 -2
View File
@@ -200,12 +200,12 @@ export const OFFICIAL_TOOL_CONTRACTS = [
),
official(
"search_procedures",
"Search every analyzed procedure name using literal matching by default or regex when requested. Results are deterministic and complete. Hopper regex mode uses ECMAScript Unicode syntax in a cancellable worker; a matching deadline leaves its API usable. Select detail summary for counts and retained native-inventory pages; complete remains the default.",
"Search every analyzed procedure name using literal matching by default or regex when requested. Results are deterministic and complete. Hopper regex mode uses ECMAScript Unicode syntax in a cancellable worker with a fixed 5000 ms matching deadline; this release has no runtime override, and a matching deadline leaves its API usable. Select detail summary for counts and retained native-inventory pages; complete remains the default.",
z.object({ ...analysisSearchInput, detail: analysisDetailSchema }),
),
official(
"search_strings",
"Search every provider-reported string object or candidate using literal matching by default or regex when requested. Results are deterministic and complete over that provider inventory, not every text sequence in artifact bytes. Kuna scans mapped sequences of at least five characters, including unterminated fragments, without computing references. Ghidra may omit packed or undefined literals, including Rust strings; an empty result does not prove text is absent. If an address is known, read_bytes and xrefs inspect its bytes and references. Hopper retains typed byte decoding, native display, and explicit decoding unknowns. Adjacent long-literal fragments match independently. Hopper regex mode uses ECMAScript Unicode syntax in a cancellable worker; a matching deadline leaves its API usable. Select detail summary for counts and retained native-inventory pages; complete remains the default.",
"Search every provider-reported string object or candidate using literal matching by default or regex when requested. Results are deterministic and complete over that provider inventory, not every text sequence in artifact bytes. Kuna scans mapped sequences of at least five characters, including unterminated fragments, without computing references. Ghidra may omit packed or undefined literals, including Rust strings; an empty result does not prove text is absent. If an address is known, read_bytes and xrefs inspect its bytes and references. Hopper retains typed byte decoding, native display, and explicit decoding unknowns. Adjacent long-literal fragments match independently. Hopper regex mode uses ECMAScript Unicode syntax in a cancellable worker with a fixed 5000 ms matching deadline; this release has no runtime override, and a matching deadline leaves its API usable. Select detail summary for counts and retained native-inventory pages; complete remains the default.",
z.object({ ...analysisSearchInput, detail: analysisDetailSchema }),
),
official(
+1 -1
View File
@@ -95,7 +95,7 @@ export const SESSION_TOOL_CONTRACTS = [
),
session(
"capture_process_scenario",
"Run one caller-selected command under a PTY and return process capture Evidence with residual unknowns. The renderer admits at most 1,000,000 combined terminal cells, computed as columns × (rows + scrollback); every resize uses the selected scrollback and must fit the same budget. Supply a smaller terminal shape when that product is too large; dimensions are never clamped. A command name resolves through the inherited PATH; the working directory defaults to the caller's current directory; host environment variables are inherited with scenario overrides. Environment keys cannot contain '=' or NUL, and REA_PROCESS_RUN_ID is reserved for process ownership. Filesystem snapshots are opt-in through filesystem_observation_paths. A positive finalization_ms makes a timeout or idle deadline send SIGTERM to the captured root through its start identity, keep observing, and send SIGKILL once after the interval; exit.finalization records each attempt with its delivery result and the observed exit, and cancellation still ends the run at once. The target runs with the current user's permissions; this is not a security sandbox.",
"Run one caller-selected command under a PTY and return process capture Evidence with residual unknowns. The renderer admits at most 1,000,000 combined terminal cells, computed as columns × (rows + scrollback); every resize uses the selected scrollback and must fit the same budget. Supply a smaller terminal shape when that product is too large; dimensions are never clamped. This release has no runtime override for that cell budget. A command name resolves through the inherited PATH; the working directory defaults to the caller's current directory; host environment variables are inherited with scenario overrides. Environment keys cannot contain '=' or NUL, and REA_PROCESS_RUN_ID is reserved for process ownership. Filesystem snapshots are opt-in through filesystem_observation_paths. A positive finalization_ms makes a timeout or idle deadline send SIGTERM to the captured root through its start identity, keep observing, and send SIGKILL once after the interval; exit.finalization records each attempt with its delivery result and the observed exit, and cancellation still ends the run at once. The target runs with the current user's permissions; this is not a security sandbox.",
processScenarioSchema,
),
session(
+13
View File
@@ -54,3 +54,16 @@ export const fixedAllowanceOptions = (
retryable: false,
fixedAllowance: { name: allowance, kind },
});
/**
* Name a fixed producer input allowance in bytes. Host and inherited limits
* do not raise it, and this release has no runtime override.
*/
export const fixedProducerInputAllowance = (
bytes: number,
allowance: string,
): { readonly retryable: false; readonly fixedAllowance: FixedAllowance } =>
fixedAllowanceOptions(
`${allowance} of ${String(bytes)} bytes (this release has no runtime override)`,
"input",
);
+16 -1
View File
@@ -1,5 +1,6 @@
import { expect, it } from "vitest";
import { SESSION_TOOL_CONTRACTS } from "../../contracts/sessionToolContracts.js";
import {
digestProcessCommitment,
parseProcessScenario,
@@ -151,6 +152,20 @@ it("admits the default and the exact combined terminal-cell budget", () => {
).toBe(true);
});
it("tells the agent the terminal cell budget has no runtime override", () => {
const description = SESSION_TOOL_CONTRACTS.find(
(tool) => tool.name === "capture_process_scenario",
)?.description;
expect(PROCESS_TERMINAL_CELL_BUDGET).toBe(1_000_000);
expect(description).toContain("1,000,000 combined terminal cells");
expect(description).toContain(
"This release has no runtime override for that cell budget.",
);
expect(processScenarioSchema.shape.terminal.unwrap().description).toContain(
"This release has no runtime override.",
);
});
it("rejects an oversized initial buffer without clamping caller dimensions", () => {
const input = {
executable: "/bin/true",
@@ -165,7 +180,7 @@ it("rejects an oversized initial buffer without clamping caller dimensions", ()
code: "custom",
path: ["terminal"],
message: expect.stringContaining(
`${String(PROCESS_TERMINAL_CELL_BUDGET)} renderer cells`,
`${String(PROCESS_TERMINAL_CELL_BUDGET)} renderer cells. This release has no runtime override.`,
),
}),
);
+3 -3
View File
@@ -164,7 +164,7 @@ export const processScenarioSchema = z
scrollback: z.number().int().safe().nonnegative().default(1_000),
})
.describe(
"The visible terminal and scrollback together must use at most 1,000,000 xterm renderer cells: columns × (rows + scrollback). Resize events use the same scrollback and limit.",
"The visible terminal and scrollback together must use at most 1,000,000 xterm renderer cells: columns × (rows + scrollback). Resize events use the same scrollback and limit. This release has no runtime override.",
)
.default({ columns: 80, rows: 24, scrollback: 1_000 }),
events: z
@@ -268,7 +268,7 @@ export const processScenarioSchema = z
context.addIssue({
code: "custom",
path: ["terminal"],
message: `terminal columns × (rows + scrollback) must not exceed ${String(PROCESS_TERMINAL_CELL_BUDGET)} renderer cells`,
message: `terminal columns × (rows + scrollback) must not exceed ${String(PROCESS_TERMINAL_CELL_BUDGET)} renderer cells. This release has no runtime override.`,
});
for (let index = 1; index < scenario.events.length; index += 1) {
const event = scenario.events[index];
@@ -293,7 +293,7 @@ export const processScenarioSchema = z
context.addIssue({
code: "custom",
path: ["events", index],
message: `resize event at ${String(event.at_ms)} ms exceeds the ${String(PROCESS_TERMINAL_CELL_BUDGET)}-cell renderer budget with the selected scrollback`,
message: `resize event at ${String(event.at_ms)} ms exceeds the ${String(PROCESS_TERMINAL_CELL_BUDGET)}-cell renderer budget with the selected scrollback. This release has no runtime override.`,
});
if (event.at_ms > scenario.timeout_ms) {
context.addIssue({
+75 -1
View File
@@ -491,6 +491,77 @@ it.each([false, true])(
},
);
it("names the fixed producer command deadline and says it has no override", async () => {
const resources = new ArtifactAnalysisResources(
"fixture",
"inspect_packet_fields",
readStableArtifact,
readLinuxResourceLimits,
{ message: "busy", details: {} },
);
const failure = new OwnedCommandFailure("timeout", "command timed out", null);
try {
const error = resources.commandFailure(
failure,
{ command_timeout_ms: 30_000, output_bytes: 1024 },
undefined,
);
const projected = projectAnalysisError(error);
expect(projected.message).toContain("30000 ms");
expect(projected.remediation.action).toContain(
"this release has no runtime override",
);
expect(projected.remediation.action).not.toContain(
"REA_SWIFTC_COMPILE_TIMEOUT_MS",
);
} finally {
await resources.close();
}
});
it("says REA retains the tighter inherited limit when a producer signal fires", async () => {
const resources = new ArtifactAnalysisResources(
"fixture",
"inspect_packet_fields",
readStableArtifact,
readLinuxResourceLimits,
{ message: "busy", details: {} },
);
const empty = { text: "", bytes: 0, observedBytes: 0 };
const failure = new OwnedCommandFailure("process", "producer signal", {
stdout: empty,
stderr: empty,
exitCode: null,
signal: "SIGXCPU",
});
try {
const error = resources.commandFailure(
failure,
{ command_timeout_ms: 30_000, output_bytes: 4_194_304 },
{
address_space_bytes: 1_073_741_824,
file_bytes: 67_108_864,
cpu_seconds: 30,
},
);
const projected = projectAnalysisError(error);
expect(projected.remediation.action).toContain(
"REA retains the tighter inherited limit",
);
expect(projected.remediation.action).toContain("1073741824 bytes");
expect(projected.remediation.action).toContain("67108864 bytes");
expect(projected.remediation.action).toContain("30 CPU seconds");
expect(projected.remediation.action).toContain(
"This release has no runtime override",
);
expect(projected.details).toMatchObject({
reported_limits: { cpu_seconds: 30 },
});
} finally {
await resources.close();
}
});
it("treats a fixed command-output cap as a non-retryable capture allowance", async () => {
const resources = new ArtifactAnalysisResources(
"fixture",
@@ -519,7 +590,10 @@ it("treats a fixed command-output cap as a non-retryable capture allowance", asy
const projected = projectAnalysisError(error);
expect(projected.retryable).toBe(false);
expect(projected.remediation.action).toContain(
"The retained command-output allowance",
"The retained command-output allowance of 1024 bytes",
);
expect(projected.remediation.action).toContain(
"this release has no runtime override",
);
expect(projected.remediation.action).not.toMatch(
/Review the reported worker memory limits/u,
+19 -10
View File
@@ -352,11 +352,11 @@ export class ArtifactAnalysisResources {
cause.cancellation === undefined ? cause : cause.cancellation.reason,
});
if (cause.reason === "timeout")
return new AnalysisTimeoutError(
operation,
limits.command_timeout_ms,
detail,
);
return new AnalysisTimeoutError(operation, limits.command_timeout_ms, {
...detail,
userMessage: `The owned producer command reached its fixed ${String(limits.command_timeout_ms)} ms deadline. This is not a client-side timeout; extending only the MCP client's deadline does not change this budget.`,
remediationAction: `REA's fixed producer command deadline is ${String(limits.command_timeout_ms)} ms and this release has no runtime override. Inspect any partial output and cleanup before retrying. Repeating the unchanged request reaches the same deadline.`,
});
if (cause.reason === "output-limit")
return new AnalysisResourceConstraintError(
operation,
@@ -366,7 +366,7 @@ export class ArtifactAnalysisResources {
{
...detail,
...fixedAllowanceOptions(
"The retained command-output allowance",
`The retained command-output allowance of ${String(limits.output_bytes)} bytes (this release has no runtime override)`,
"capture",
),
},
@@ -375,14 +375,23 @@ export class ArtifactAnalysisResources {
cause.reason === "process" &&
(cause.snapshot?.signal === "SIGXFSZ" ||
cause.snapshot?.signal === "SIGXCPU")
)
) {
const signal = cause.snapshot.signal;
const reported =
effectiveLimits === undefined
? "The effective inherited limit was not reported."
: `The effective inherited limits are ${String(effectiveLimits.address_space_bytes)} bytes of address space, ${String(effectiveLimits.file_bytes)} bytes per file, and ${String(effectiveLimits.cpu_seconds)} CPU seconds.`;
return new AnalysisResourceConstraintError(
operation,
cause.snapshot.signal === "SIGXFSZ" ? "file-size" : "cpu",
`Producer terminated with ${cause.snapshot.signal}; the signal's exact cause is not independently established.`,
signal === "SIGXFSZ" ? "file-size" : "cpu",
`Producer terminated with ${signal}; the signal's exact cause is not independently established.`,
effectiveLimits === undefined ? null : { ...effectiveLimits },
detail,
{
...detail,
remediationAction: `${reported} REA retains the tighter inherited limit. This release has no runtime override for that ceiling. A smaller artifact may finish inside the retained limit. Repeating the unchanged request fails the same way.`,
},
);
}
return new AnalysisOutputError(operation, cause.message, detail);
}
@@ -1,6 +1,10 @@
import { describe, expect, it } from "vitest";
import { resolveSwiftcCompileTimeoutMs } from "./DarwinProcessRunTokenReader.js";
import {
resolveSwiftcCompileTimeoutMs,
swiftcCompileTimeoutRecovery,
} from "./DarwinProcessRunTokenReader.js";
import { MAX_PROVIDER_STARTUP_TIMEOUT_MS } from "../config/providerStartupTimeout.js";
describe("resolveSwiftcCompileTimeoutMs", () => {
it("keeps the historical default when unset", () => {
@@ -31,4 +35,14 @@ describe("resolveSwiftcCompileTimeoutMs", () => {
}),
).toBe(120_000);
});
it("names the launch-environment deadline, range, and restart on a timeout", () => {
const recovery = swiftcCompileTimeoutRecovery(300_000);
expect(recovery).toContain("REA_SWIFTC_COMPILE_TIMEOUT_MS");
expect(recovery).toContain("300000 ms");
expect(recovery).toContain(`1..${String(MAX_PROVIDER_STARTUP_TIMEOUT_MS)}`);
expect(recovery).toContain("120000 ms");
expect(recovery).toContain("restart REA");
expect(recovery).toContain("next ownership-helper compile");
});
});
+19 -4
View File
@@ -7,6 +7,7 @@ import { z } from "zod";
import {
DEFAULT_SWIFTC_COMPILE_TIMEOUT_MS,
MAX_PROVIDER_STARTUP_TIMEOUT_MS,
providerStartupTimeoutSchema,
} from "../config/providerStartupTimeout.js";
import { err, ok, type Result } from "../domain/result.js";
@@ -105,7 +106,9 @@ export const SWIFTC_INTERRUPT = "SIGINT";
* Swift SDK cache on the supported Intel macOS VM (about 88s), so compilation
* stays bounded independently of short provider version probes. Invalid ambient
* values fall back to the default; `parseConfig` still rejects them at CLI/MCP
* admission.
* admission. The resolved value is captured when the reader is created.
* Production creates that reader from the launch environment at process start,
* so a change applies after REA restarts, on the next ownership-helper compile.
*/
export const resolveSwiftcCompileTimeoutMs = (
options: {
@@ -121,6 +124,10 @@ export const resolveSwiftcCompileTimeoutMs = (
return parsed.success ? parsed.data : DEFAULT_SWIFTC_COMPILE_TIMEOUT_MS;
};
/** Name the existing swiftc deadline when a helper compile times out. */
export const swiftcCompileTimeoutRecovery = (timeoutMs: number): string =>
`The effective REA_SWIFTC_COMPILE_TIMEOUT_MS value is ${String(timeoutMs)} ms (integer range 1..${String(MAX_PROVIDER_STARTUP_TIMEOUT_MS)}; omitted default ${String(DEFAULT_SWIFTC_COMPILE_TIMEOUT_MS)} ms). Set it in the environment that starts REA and restart REA; the next ownership-helper compile uses the new value.`;
/** Bound on compiler diagnostics embedded in the inspection failure text. */
const STDERR_EXCERPT_LIMIT = 512;
@@ -272,8 +279,11 @@ const compilerOutcomeDescription = (
if (outcome === undefined) return `compiler result: ${String(fallbackCode)}`;
if (outcome.outcome === "exited")
return `compiler result: exit code ${String(outcome.code)}`;
if (outcome.outcome === "timed_out")
return `compiler result: timed out after ${String(outcome.timeoutMs)} ms`;
if (outcome.outcome === "timed_out") {
if (outcome.timeoutMs === undefined)
return "compiler result: timed out. The effective REA_SWIFTC_COMPILE_TIMEOUT_MS value was not reported.";
return `compiler result: timed out after ${String(outcome.timeoutMs)} ms. ${swiftcCompileTimeoutRecovery(outcome.timeoutMs)}`;
}
if (outcome.outcome === "signaled")
return `compiler result: stopped by signal ${outcome.signal ?? "an unknown signal"}`;
return `compiler result: ${String(outcome.code)}`;
@@ -302,8 +312,13 @@ const compilerInspectionError = (
stderr.truncated ||
outcome?.stderrTruncated === true ||
outcome?.outputTruncated === true;
const truncationNote = !truncated
? ""
: stderr.truncated
? `… [diagnostics truncated at ${String(STDERR_EXCERPT_LIMIT)} bytes; this release has no runtime override]`
: "… [diagnostics truncated; this release has no runtime override]";
const suffix = diagnostics
? `: ${diagnostics}${truncated ? "… [diagnostics truncated]" : ""}`
? `: ${diagnostics}${truncationNote}`
: outcome === undefined
? `: ${cause instanceof Error ? cause.message : String(cause)}`
: ": no compiler diagnostics were captured";
+10 -11
View File
@@ -328,6 +328,13 @@ const mergeUnverifiedObservations = (
return { ...result, unverified: merged };
};
/**
* Times one group may be revalidated in a single cleanup. The second pass
* covers a group that reappears after it was already processed. Another
* appearance stays unresolved instead of reporting success.
*/
const OWNED_GROUP_REVALIDATION_LIMIT = 2;
/** Token-validate every rooted POSIX process group before signaling any. */
export const cleanupOwnedProcessGroup = async (
ownership: OwnedProcessGroup,
@@ -360,7 +367,7 @@ export const cleanupOwnedProcessGroup = async (
return mergeUnverifiedObservations(
{
cleaned: false,
reason: `owned process group ${String(processGroupId)} reappeared after revalidation`,
reason: `owned process group ${String(processGroupId)} reappeared after ${String(OWNED_GROUP_REVALIDATION_LIMIT)} revalidation passes. This release has no runtime override.`,
},
plan.unverified,
);
@@ -380,7 +387,7 @@ export const cleanupOwnedProcessGroup = async (
return mergeUnverifiedObservations(
{
cleaned: false,
reason: `owned process group ${String(groupId)} reappeared after revalidation`,
reason: `owned process group ${String(groupId)} reappeared after ${String(OWNED_GROUP_REVALIDATION_LIMIT)} revalidation passes. This release has no runtime override.`,
},
plan.unverified,
);
@@ -631,13 +638,6 @@ const isExitRefreshCandidate = (
);
};
/**
* Times one group may be revalidated in a single cleanup. The second pass
* covers a group that reappears after it was already processed. Another
* appearance stays unresolved instead of reporting success.
*/
const OWNED_GROUP_REVALIDATION_LIMIT = 2;
/**
* Token-check rows that exist only in the post-exit table. A prior PID, group,
* or ancestry link is handled by the caller; these rows have none of those.
@@ -716,8 +716,7 @@ const classifyUnlinkedExitRows = async (
if (confirmations > EXIT_REFRESH_CONFIRMATION_LIMIT)
return {
kind: "uncertain",
reason:
"process ownership could not be revalidated: exit refresh confirmation limit exceeded",
reason: `process ownership could not be revalidated: the exit refresh confirmation limit of ${String(EXIT_REFRESH_CONFIRMATION_LIMIT)} reads was exceeded. This release has no runtime override.`,
};
// The confirmation read is its own table. A PID classified on the previous
// table, including a readable foreign token, does not identify the process
@@ -592,7 +592,7 @@ describe("identityless exit refresh keeps later confirmation rows", () => {
).resolves.toMatchObject({
cleaned: false,
reason:
"process ownership could not be revalidated: exit refresh confirmation limit exceeded",
"process ownership could not be revalidated: the exit refresh confirmation limit of 8 reads was exceeded. This release has no runtime override.",
});
expect(signalGroup).not.toHaveBeenCalled();
});
@@ -875,7 +875,8 @@ describe("identityless cleanup revalidates a group that reappears", () => {
),
).resolves.toMatchObject({
cleaned: false,
reason: "owned process group 101 reappeared after revalidation",
reason:
"owned process group 101 reappeared after 2 revalidation passes. This release has no runtime override.",
});
expect(signalGroup.mock.calls).toEqual([[100, "SIGKILL"]]);
});
+33 -23
View File
@@ -14,7 +14,37 @@ import { err, ok, type Result } from "../domain/result.js";
const MATCHING_DEADLINE_MS = 5_000;
const WORKER_HEAP_MIB = 128;
/** Shared capability text for the supervised ECMAScript matching boundary. */
export const REGEX_MATCHING_LIMITATION = `Regex mode uses ECMAScript Unicode semantics in a cancellable worker with a ${String(MATCHING_DEADLINE_MS / 1000)}-second matching deadline; matching failure leaves the service available.`;
export const REGEX_MATCHING_LIMITATION = `Regex mode uses ECMAScript Unicode semantics in a cancellable worker with a ${String(MATCHING_DEADLINE_MS / 1000)}-second matching deadline and a ${String(WORKER_HEAP_MIB)} MiB worker heap. This release has no runtime override for either cap; matching failure leaves the service available.`;
/** Fixed regex deadline. The worker heap and this deadline are separate caps. */
export const regexMatchingDeadlineError = (
operation: string,
): AnalysisResourceConstraintError =>
new AnalysisResourceConstraintError(
operation,
"cpu",
`Regex matching exceeded its supervised deadline of ${String(MATCHING_DEADLINE_MS)} ms. This release has no runtime override. The service remains available.`,
{ matching_deadline_ms: MATCHING_DEADLINE_MS },
{
remediationAction: `The regex matching deadline is ${String(MATCHING_DEADLINE_MS)} ms and this release has no runtime override. Simplify the ECMAScript Unicode regex or use literal mode, then retry in the same session.`,
},
);
/** Fixed regex worker heap. Inherited limits and host memory do not raise it. */
export const regexWorkerHeapError = (
operation: string,
cause: Error,
): AnalysisResourceConstraintError =>
new AnalysisResourceConstraintError(
operation,
"memory",
`Regex worker exhausted its ${String(WORKER_HEAP_MIB)} MiB heap budget. This release has no runtime override.`,
{ worker_heap_mib: WORKER_HEAP_MIB },
{
cause,
remediationAction: `The regex worker heap is ${String(WORKER_HEAP_MIB)} MiB and this release has no runtime override. Inherited limits and additional host memory do not raise it. Simplify the pattern or use literal mode.`,
},
);
const replySchema = z.discriminatedUnion("status", [
z.strictObject({
status: z.literal("matched"),
@@ -84,21 +114,7 @@ export const matchRegexValues = async (input: {
const onAbort = () =>
finish(err(new AnalysisCancelledError(input.operation)));
const timer = setTimeout(
() =>
finish(
err(
new AnalysisResourceConstraintError(
input.operation,
"cpu",
"Regex matching exceeded its supervised deadline; the service remains available.",
{ matching_deadline_ms: MATCHING_DEADLINE_MS },
{
remediationAction:
"Simplify the ECMAScript Unicode regex or use literal mode, then retry in the same session.",
},
),
),
),
() => finish(err(regexMatchingDeadlineError(input.operation))),
MATCHING_DEADLINE_MS,
);
input.signal?.addEventListener("abort", onAbort, { once: true });
@@ -144,13 +160,7 @@ export const matchRegexValues = async (input: {
finish(
err(
cause.code === "ERR_WORKER_OUT_OF_MEMORY"
? new AnalysisResourceConstraintError(
input.operation,
"memory",
"Regex worker exhausted its heap budget",
{ worker_heap_mib: WORKER_HEAP_MIB },
{ cause },
)
? regexWorkerHeapError(input.operation, cause)
: new AnalysisOutputError(
input.operation,
`Regex worker failed: ${cause.message}`,
+1 -1
View File
@@ -233,7 +233,7 @@ export const stopOwnedDebuggerProcess = async (
if (detached === undefined)
return {
status: "incomplete",
reason: `Owned ${debuggerName} inferior detach was not verified within ${DETACH_TIMEOUT_MS} ms.`,
reason: `Owned ${debuggerName} inferior detach was not verified within ${String(DETACH_TIMEOUT_MS)} ms. This release has no runtime override.`,
};
if (!isVerifiedDetachResult(detached)) {
detachOutcomes.set(session, "unconfirmed");
+9 -3
View File
@@ -47,7 +47,10 @@ import {
AnalysisResourceConstraintError,
} from "../domain/analysisErrorCore.js";
import { createEvidence } from "../domain/evidence.js";
import { fixedAllowanceOptions } from "../domain/fixedAllowanceRemediation.js";
import {
fixedAllowanceOptions,
fixedProducerInputAllowance,
} from "../domain/fixedAllowanceRemediation.js";
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
import { windowAnnotationText } from "../domain/signals/annotationTextWindow.js";
import {
@@ -370,7 +373,7 @@ export class SigrokRecordingProvider implements SignalRecordingPort {
"Annotations are the selected decoder's interpretation of the recorded samples and caller-selected settings, not independently verified protocol semantics. Empty output does not establish absence of traffic.",
"Sample-number ranges and display text are preserved verbatim, not converted to timestamps or parsed as a lossless byte stream. All producer stderr is retained, including warnings on exit zero.",
"Only a private copy of the selected file is decoded. Hardware discovery/acquisition and target execution are not requested. Caller-supplied tools, plugins and decoder modules are trusted; this is not an OS sandbox.",
"Linux prlimit enforces soft and hard ceilings before execution, preserving stricter inherited limits: at most 1 GiB address space per process, 64 MiB per file, 30 CPU seconds and no core dumps. Effective limits are reported. Resident input is capped at 32 MiB, each command at 30 wall-clock seconds and combined output at 4 MiB. Output overflow fails. These are per-process/per-file limits, not aggregate memory or filesystem quotas; caller-supplied tools and the limiter are trusted.",
"Linux prlimit enforces soft and hard ceilings before execution, preserving stricter inherited limits: at most 1 GiB address space per process, 64 MiB per file, 30 CPU seconds and no core dumps. Effective limits are reported. Resident input is capped at 32 MiB, each command at 30 wall-clock seconds and combined output at 4 MiB. Output overflow fails. These are per-process/per-file limits, not aggregate memory or filesystem quotas; caller-supplied tools and the limiter are trusted. REA retains the tighter inherited limit. This release has no runtime override.",
],
}),
);
@@ -545,7 +548,10 @@ export class SigrokRecordingProvider implements SignalRecordingPort {
{ input_bytes: LIMITS.input_bytes },
{
cause,
...fixedAllowanceOptions("the fixed sigrok input allowance", "input"),
...fixedProducerInputAllowance(
LIMITS.input_bytes,
"the fixed sigrok input allowance",
),
},
);
if (cause instanceof ArtifactReaderFailure && cause.reason === "integrity")
@@ -478,6 +478,9 @@ it("preserves termination signal and effective file ceiling as a resource failur
reportedLimits: { file_bytes: 1048576 },
capturedOutput: { signal: "SIGXFSZ", stderr: "before file limit\n" },
});
expect(analysisErrorRemediationAction(result.error)).toContain(
"REA retains the tighter inherited limit",
);
});
it("rejects oversized selector lists and UTF-8 argv expansion before provider work", async () => {
@@ -27,6 +27,7 @@ import {
createDarwinProcessRunTokenReader,
DarwinProcessOwnershipInspectionError,
SWIFTC_INTERRUPT,
swiftcCompileTimeoutRecovery,
} from "../../../src/process/DarwinProcessRunTokenReader.js";
import { execFileOutput } from "../../../src/process/ExecFileOutput.js";
import {
@@ -219,7 +220,9 @@ it.skipIf(process.platform === "win32")(
expect(failure.message).toContain(
"(compiler result: exit code 1): module cache is not writable",
);
expect(failure.message).toContain("[diagnostics truncated]");
expect(failure.message).toContain(
"[diagnostics truncated at 512 bytes; this release has no runtime override]",
);
const normalized = normalizeCaptureFailure(failure, undefined);
if (!(normalized instanceof ProcessCaptureError))
throw new Error("Expected a normalized process capture error");
@@ -260,14 +263,18 @@ it.skipIf(process.platform === "win32")(
compileTimeoutMs: 3_000,
});
try {
await expect(preparationFailure(reader)).resolves.toMatchObject({
message:
"macOS process ownership helper compilation failed via xcrun (compiler result: timed out after 3000 ms): error: PCH was compiled with module cache path that differs",
compilerOutcome: {
outcome: "timed_out",
code: "ETIMEDOUT",
timeoutMs: 3_000,
},
const failure = await preparationFailure(reader);
expect(failure.message).toContain(
"macOS process ownership helper compilation failed via xcrun (compiler result: timed out after 3000 ms.",
);
expect(failure.message).toContain(swiftcCompileTimeoutRecovery(3_000));
expect(failure.message).toContain(
"error: PCH was compiled with module cache path that differs",
);
expect(failure.compilerOutcome).toMatchObject({
outcome: "timed_out",
code: "ETIMEDOUT",
timeoutMs: 3_000,
});
} finally {
await reader.close();
@@ -329,7 +336,7 @@ it.skipIf(process.platform === "win32")(
expect(outcome.stderrBytes).toBe(601);
expect(outcome.stderrTruncated).toBe(false);
expect(failure.message).toContain(
`: ${"x".repeat(512)}… [diagnostics truncated]`,
`: ${"x".repeat(512)}… [diagnostics truncated at 512 bytes; this release has no runtime override]`,
);
const excerpt = failure.message.split("): ")[1]?.split("…")[0];
expect(excerpt).toBe("x".repeat(512));
@@ -723,7 +723,8 @@ it("retains an owned debugger when inferior detach does not settle", async () =>
await vi.advanceTimersByTimeAsync(1_000);
expect(await attempt).toEqual({
status: "incomplete",
reason: "Owned GDB inferior detach was not verified within 1000 ms.",
reason:
"Owned GDB inferior detach was not verified within 1000 ms. This release has no runtime override.",
});
const retry = stopOwnedDebuggerProcess(session, "GDB", {
platform: "linux",
@@ -731,7 +732,8 @@ it("retains an owned debugger when inferior detach does not settle", async () =>
await vi.advanceTimersByTimeAsync(1_000);
expect(await retry).toEqual({
status: "incomplete",
reason: "Owned GDB inferior detach was not verified within 1000 ms.",
reason:
"Owned GDB inferior detach was not verified within 1000 ms. This release has no runtime override.",
});
expect(detachCalls).toBe(1);
expect(signals).toEqual([]);
+37 -1
View File
@@ -2,7 +2,43 @@ import { expect, it } from "vitest";
import { ok } from "../../../src/domain/result.js";
import { HopperRegexSearch } from "../../../src/hopper/HopperRegexSearch.js";
import { matchRegexValues } from "../../../src/process/RegexMatcher.js";
import { OFFICIAL_TOOL_CONTRACTS } from "../../../src/contracts/officialToolContracts.js";
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
import {
matchRegexValues,
REGEX_MATCHING_LIMITATION,
regexMatchingDeadlineError,
regexWorkerHeapError,
} from "../../../src/process/RegexMatcher.js";
it("names the fixed regex deadline and heap and says neither has an override", () => {
expect(REGEX_MATCHING_LIMITATION).toContain("5-second matching deadline");
expect(REGEX_MATCHING_LIMITATION).toContain("128 MiB");
expect(REGEX_MATCHING_LIMITATION).toContain(
"This release has no runtime override",
);
const deadline = projectAnalysisError(
regexMatchingDeadlineError("search_procedures"),
);
expect(deadline.message).toContain("5000 ms");
expect(deadline.remediation.action).toContain(
"this release has no runtime override",
);
const heap = projectAnalysisError(
regexWorkerHeapError("search_procedures", new Error("heap")),
);
expect(heap.message).toContain("128 MiB");
expect(heap.remediation.action).toContain(
"additional host memory do not raise it",
);
for (const name of ["search_procedures", "search_strings"] as const) {
const description = OFFICIAL_TOOL_CONTRACTS.find(
(tool) => tool.name === name,
)?.description;
expect(description).toContain("5000 ms");
expect(description).toContain("no runtime override");
}
});
it("preserves Unicode matching order and refuses a pre-cancelled match", async () => {
expect(
@@ -609,6 +609,9 @@ it("preserves termination signal and effective file ceiling as a resource failur
reportedLimits: { file_bytes: 1048576 },
capturedOutput: { signal: "SIGXFSZ", stderr: "before file limit\n" },
});
expect(analysisErrorRemediationAction(result.error)).toContain(
"REA retains the tighter inherited limit",
);
});
it("discovers decoders without reading a recording and retains installation provenance", async () => {