diff --git a/README.md b/README.md index 4ecb3f76..8156867c 100644 --- a/README.md +++ b/README.md @@ -631,9 +631,9 @@ Process Capture records behavior and is not a security sandbox. Capture a scenario or compare two saved Process Capture Evidence records: ```bash -rea capture-process ./scenario.json > authority.json -rea capture-process ./reconstruction.json > reconstruction.json -rea compare-process-captures authority.json reconstruction.json +rea capture-process ./authority-scenario.json --json > authority-capture.json +rea capture-process ./reconstruction-scenario.json --json > reconstruction-capture.json +rea compare-process-captures authority-capture.json reconstruction-capture.json --json ``` The comparison reports each observed dimension separately and identifies the @@ -641,9 +641,16 @@ first terminal, interaction, exit, filesystem, or process divergence. See [Process Capture](docs/process-capture.md) for scenario fields, limits, and evidence boundaries. -REA installs a prebuilt PTY backend for supported macOS, Linux, and Windows -architectures. If the capability check reports that the backend is unavailable, -reinstall REA for the current platform and architecture. +Process Capture currently runs on Linux and macOS with a working native PTY +backend. Native Windows capture remains unavailable until the PTY adapter can +verify descendant cleanup; installing or reinstalling its Windows PTY binary +does not enable capture. For Linux commands, run Linux REA inside WSL. +Comparing existing capture Evidence remains available on Windows. + +On supported capture hosts, a missing or incompatible PTY binary can be fixed +by reinstalling REA for the current platform, architecture, and Node.js version +with optional dependencies enabled. Use separate scenario and output files; +`--json` produces the JSON consumed by the comparison command. ASAR inventory verifies Electron integrity metadata for both archive entries and `.asar.unpacked` companion files. Integrity failures identify the logical diff --git a/docs/process-capture.md b/docs/process-capture.md index 476cce89..8ed63020 100644 --- a/docs/process-capture.md +++ b/docs/process-capture.md @@ -12,6 +12,24 @@ sandbox. Filesystem observation paths select what REA snapshots; they do not restrict what the process can read or write. The inherited environment is not recorded, so its influence may remain unknown. +## Host support + +Capturing a new scenario currently requires Linux or macOS and a working +native PTY backend. Native Windows capture is unavailable because the PTY +adapter does not yet verify descendant cleanup. The native Job Object controls +used by other REA providers do not establish PTY capture support. Reinstalling +the Windows PTY binary does not enable this workflow. + +For Linux commands, use Linux REA inside WSL. Adapt the scenario to that host; +this does not establish capture of a native Windows process tree. Comparing +existing capture Evidence through the CLI or MCP remains available on Windows +and does not launch a PTY or the captured target. + +On supported capture hosts, a missing or incompatible native PTY binary has a +different recovery: reinstall REA for the active platform, architecture, and +Node.js version with optional dependencies enabled. Capability diagnostics +distinguish this from the Windows capture-adapter limitation. + ## Capture a command Write a JSON scenario and pass its path to the CLI: @@ -22,6 +40,8 @@ rea capture-process ./scenario.json --json > capture.json `--json` is required when saving input for JSON consumers; the default terminal format is TOON. The file contains the complete capture Evidence record. +Choose an output file distinct from the scenario input: shell redirection opens +and truncates the output before REA reads the scenario. For example: diff --git a/src/application/ProcessCaptureCapability.ts b/src/application/ProcessCaptureCapability.ts index 33abf901..e7ec0ef3 100644 --- a/src/application/ProcessCaptureCapability.ts +++ b/src/application/ProcessCaptureCapability.ts @@ -13,7 +13,7 @@ export const processCaptureOwnershipUnavailableReason = ( platform: NodeJS.Platform, ): string | undefined => platform === "win32" - ? "Windows process-tree ownership and cleanup are unavailable; REA cannot verify that descendants have stopped." + ? "Windows PTY process capture is unavailable because this adapter does not yet verify descendant cleanup. Use Linux (including WSL for Linux commands) or macOS. Reinstalling the PTY backend does not enable native Windows capture." : undefined; /** Dependencies that make the native PTY probe's host boundary testable. */ diff --git a/src/application/ProcessHarness.test.ts b/src/application/ProcessHarness.test.ts index 0f62497c..8c2d38bf 100644 --- a/src/application/ProcessHarness.test.ts +++ b/src/application/ProcessHarness.test.ts @@ -5,6 +5,7 @@ import { captureProcessScenario } from "./ProcessHarness.js"; import { settleProcessCaptureJournal } from "./ProcessCaptureLifecycle.js"; import { processCaptureSchema } from "../domain/processCapture.js"; import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../domain/processCapture.fixture.js"; +import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; it("rejects legacy replay output instead of silently discarding it", () => { expect( @@ -48,9 +49,19 @@ it("fails closed on Windows before resolving or launching scenario paths", async const result = await captureProcessScenario(scenario, undefined, "win32"); expect(result.ok).toBe(false); if (result.ok) throw new Error("expected Windows ownership refusal"); - expect(result.error).toBeInstanceOf(AnalysisCapabilityUnavailableError); + if (!(result.error instanceof AnalysisCapabilityUnavailableError)) + throw new Error("expected a capability-unavailable outcome"); expect(result.error).toMatchObject({ operation: "capture_process_scenario", - reason: expect.stringContaining("Windows process-tree ownership"), + reason: expect.stringContaining("Windows PTY process capture"), + }); + expect(projectAnalysisError(result.error)).toMatchObject({ + code: "capability_unavailable", + category: "unsupported_provider", + message: expect.stringContaining("does not yet verify descendant cleanup"), + details: { + operation: "capture_process_scenario", + reason: result.error.reason, + }, }); }); diff --git a/src/application/ProcessHarness.ts b/src/application/ProcessHarness.ts index 4ea9c827..822f8f98 100644 --- a/src/application/ProcessHarness.ts +++ b/src/application/ProcessHarness.ts @@ -409,6 +409,7 @@ export const captureProcessScenario = async ( "rea-process", "capture_process_scenario", ownershipReason, + { userMessage: ownershipReason }, ), ); try { diff --git a/src/domain/analysisErrorCore.ts b/src/domain/analysisErrorCore.ts index b9cdea56..e709dabc 100644 --- a/src/domain/analysisErrorCore.ts +++ b/src/domain/analysisErrorCore.ts @@ -52,13 +52,19 @@ export class AnalysisOutputError extends AnalysisError { /** Selected provider cannot execute a declared analysis operation. */ export class AnalysisCapabilityUnavailableError extends AnalysisError { readonly _tag = "AnalysisCapabilityUnavailableError"; + override readonly userMessage: string | undefined; constructor( readonly providerId: string, readonly operation: string, readonly reason: string, + options?: ErrorOptions & { readonly userMessage?: string }, ) { - super(`Provider ${providerId} cannot execute ${operation}: ${reason}`); + super( + `Provider ${providerId} cannot execute ${operation}: ${reason}`, + options, + ); + this.userMessage = options?.userMessage; } } diff --git a/src/domain/analysisErrorProjection.test.ts b/src/domain/analysisErrorProjection.test.ts index 1e4289ff..6b83265a 100644 --- a/src/domain/analysisErrorProjection.test.ts +++ b/src/domain/analysisErrorProjection.test.ts @@ -201,6 +201,8 @@ describe("analysis error projection: caller contract", () => { ]); expect(projected[1]).toMatchObject({ category: "unsupported_provider", + message: + "This analysis is unavailable for the current target. Choose another analysis or target.", details: { provider_id: "fixture", operation: "overview", @@ -213,6 +215,31 @@ describe("analysis error projection: caller contract", () => { expect(JSON.stringify(projected)).not.toContain("secret-token"); }); + it("uses explicit capability recovery while retaining the constraint", () => { + const projected = projectAnalysisError( + new AnalysisCapabilityUnavailableError( + "fixture", + "capture", + "host constraint", + { + userMessage: "Use the supported host workflow.", + cause: new Error("private internal cause"), + }, + ), + ); + expect(projected).toMatchObject({ + code: "capability_unavailable", + category: "unsupported_provider", + message: "Use the supported host workflow.", + details: { + provider_id: "fixture", + operation: "capture", + reason: "host constraint", + }, + }); + expect(JSON.stringify(projected)).not.toContain("private internal cause"); + }); + it("preserves exact artifact-integrity coordinates", () => { expect( projectAnalysisError( diff --git a/tests/boundary/process/processCaptureCapability.test.ts b/tests/boundary/process/processCaptureCapability.test.ts index 12b63c14..5a8e3115 100644 --- a/tests/boundary/process/processCaptureCapability.test.ts +++ b/tests/boundary/process/processCaptureCapability.test.ts @@ -54,7 +54,29 @@ describe("process capture capability diagnostics", () => { it("keeps the ownership limitation separate from native PTY support", () => { expect(processCaptureOwnershipUnavailableReason("win32")).toContain( - "process-tree ownership", + "Windows PTY process capture", + ); + }); + + it("explains Windows capture admission before loading or spawning a PTY", async () => { + let loaded = false; + const capability = await probeProcessCaptureCapability({ + platform: "win32", + loadPty: async () => { + loaded = true; + throw new Error("Windows admission must precede PTY loading"); + }, + }); + expect(loaded).toBe(false); + expect(capability).toMatchObject({ + available: false, + reason: expect.stringContaining("does not yet verify descendant cleanup"), + }); + if (capability.available) + throw new Error("expected Windows capture refusal"); + expect(capability.reason).toContain("WSL for Linux commands"); + expect(capability.reason).toContain( + "Reinstalling the PTY backend does not enable native Windows capture", ); }); });