fix(javascript): accept the Wakaru 1.x release line and verify 1.14.0 (#1540)

* fix(javascript): accept the Wakaru 1.x release line and verify 1.14.0

Exact --version equality rejected every Wakaru build except 1.13.0, and
strict report schemas rejected any added field, so each upstream release
broke recovery until REA re-pinned. Wakaru now documents that --version,
--json and provenance.json change only by addition within a major version.

Admit ^1.13.0 with the verified/compatible split used for Binwalk and
Unblob, report the observed version instead of the pinned constant, and
add a limitation for unverified releases. Parse reports with loose
objects and keep the reported module status as a string; path, identity,
range and file-set checks stay exact.

Move the audited pin, submodule, CI binary and verifier to 1.14.0, and
document where to get the binary from the refusal message.

* fix(javascript): project known Wakaru warning fields

---------

Co-authored-by: morluto <[email protected]>
This commit is contained in:
Pionxzh
2026-10-10 23:02:15 +08:00
committed by GitHub
co-authored by morluto
parent 0363e5a8e1
commit ded59689ce
16 changed files with 272 additions and 66 deletions
@@ -56,9 +56,9 @@ jobs:
shell: bash
run: |
mkdir -p "$RUNNER_TEMP/rea-wakaru"
curl --fail --location --retry 3 https://github.com/pionxzh/wakaru/releases/download/v1.13.0/wakaru-linux-x64.tar.gz -o "$RUNNER_TEMP/rea-wakaru/release.tar.gz"
curl --fail --location --retry 3 https://github.com/pionxzh/wakaru/releases/download/v1.14.0/wakaru-linux-x64.tar.gz -o "$RUNNER_TEMP/rea-wakaru/release.tar.gz"
tar -xzf "$RUNNER_TEMP/rea-wakaru/release.tar.gz" -C "$RUNNER_TEMP/rea-wakaru" wakaru
printf '%s %s\n' e8c7ca052974604197389f6ace7464e4dd24dce64e2730971521bd6606ec9c9e "$RUNNER_TEMP/rea-wakaru/wakaru" | sha256sum --check
printf '%s %s\n' 664fa06782a61eda7ed15885134b52525fd565dd77607e642062ceb6cd927c38 "$RUNNER_TEMP/rea-wakaru/wakaru" | sha256sum --check
- name: Install isolated fixture compilers
run: npm install --prefix "$RUNNER_TEMP/rea-fixture-compilers" --ignore-scripts --no-audit --no-fund [email protected] [email protected]
- run: npm run build:cached
+34 -7
View File
@@ -7,19 +7,46 @@ never executes the selected or recovered application code.
## Caller-supplied engine
The initial adapter is verified on Linux x64 with Wakaru **1.13.0**. Set
`REA_WAKARU_COMMAND` to the absolute path of an executable supplied by the caller.
The adapter uses util-linux `prlimit` (default `/usr/bin/prlimit`; override with
an absolute `REA_JAVASCRIPT_PRLIMIT_COMMAND`). REA does not install either tool.
Engine configuration is checked only when recovery is requested; other REA
capabilities remain available without it.
REA accepts Wakaru **^1.13.0** on Linux x64. The report parser is verified with
Wakaru **1.14.0**. Another release in that range is accepted, the observed
version is reported, and a limitation records that the parser was not verified
against that release. Wakaru changes `--version`, `--json` and `provenance.json`
only by addition within a major version
([compatibility policy](https://github.com/pionxzh/wakaru/blob/v1.14.0/docs/cli.md#machine-readable-output-compatibility)),
so the parser keeps unknown report fields and reported module statuses.
Set `REA_WAKARU_COMMAND` to the absolute path of an executable supplied by the
caller. The adapter uses util-linux `prlimit` (default `/usr/bin/prlimit`;
override with an absolute `REA_JAVASCRIPT_PRLIMIT_COMMAND`). REA does not install
either tool. Engine configuration is checked only when recovery is requested;
other REA capabilities remain available without it.
Unmodified upstream source is retained in `third_party/wakaru`, pinned to
`6070266d24b32951fa2fe1d2dad5b4313bd5c81b` (v1.13.0, Apache-2.0).
`8219bf5016a063d7fb215101a8c45d5959ec733f` (v1.14.0, Apache-2.0).
The result reports the executed binary's exact path, SHA-256 and observed version.
The audited source revision is distinct from `executed_source_revision: null`:
a version string cannot prove which source produced a caller-supplied binary.
### Install Wakaru
Use the official release binary, the same one CI verifies:
```sh
curl -fL https://github.com/pionxzh/wakaru/releases/download/v1.14.0/wakaru-linux-x64.tar.gz | tar -xz wakaru
export REA_WAKARU_COMMAND="$PWD/wakaru"
```
Or install it from npm. Point at the platform binary, not the Node.js shim in
`node_modules/.bin`:
```sh
npm install [email protected]
export REA_WAKARU_COMMAND="$(realpath node_modules/@wakaru/cli-linux-x64/wakaru)"
```
Both give the same binary (SHA-256
`664fa06782a61eda7ed15885134b52525fd565dd77607e642062ceb6cd927c38`).
## Recover and analyze
```sh
+4 -2
View File
@@ -876,12 +876,14 @@ Neither optional toolchain is a base-lane prerequisite. See
Build once with `npm run build:cached`, then run `npm run verify:javascript:recovery`.
This focused lane requires Linux x64, util-linux `prlimit`,
`REA_WAKARU_COMMAND` pointing to the official Wakaru 1.13.0 Linux x64 binary,
`REA_WAKARU_COMMAND` pointing to the official Wakaru 1.14.0 Linux x64 binary,
and `REA_JAVASCRIPT_FIXTURE_TOOLS` pointing to an isolated npm prefix containing
esbuild 0.25.10 and webpack 5.101.3. No global installation is required.
The lane compiles source-owned fixtures, exercises CLI and stdio MCP, verifies
published bytes and provenance, feeds recovered modules into existing analysis,
and compares a finite set of known fixture results. It does not establish
and compares a finite set of known fixture results. The provider also accepts
other ^1.13.0 releases and reports them as unverified; this lane proves the
audited release. It does not establish
arbitrary recovered-application equivalence. CI installs these prerequisites only
in `.github/workflows/real-javascript-recovery.yml`; the existing `real-browser`
lane uses real Chrome for browser capture and website workflows.
+3 -3
View File
@@ -15,10 +15,10 @@ import { buildRecoveryFixtures } from "../../fixtures/javascript-recovery/build.
import { mcpTextValue } from "../../lib/mcp-verifier-results.mjs";
const OFFICIAL_BINARY_SHA256 =
"e8c7ca052974604197389f6ace7464e4dd24dce64e2730971521bd6606ec9c9e";
"664fa06782a61eda7ed15885134b52525fd565dd77607e642062ceb6cd927c38";
if (!process.env.REA_WAKARU_COMMAND)
throw new Error(
"Provide REA_WAKARU_COMMAND for the official Wakaru 1.13.0 Linux x64 binary",
"Provide REA_WAKARU_COMMAND for the official Wakaru 1.14.0 Linux x64 binary",
);
const entrypoint =
process.argv[2] ?? fileURLToPath(new URL("../../rea.mjs", import.meta.url));
@@ -130,7 +130,7 @@ try {
}
}
finalReport = {
engine: "wakaru 1.13.0",
engine: "wakaru 1.14.0",
toolchains: fixtures.toolchains,
results: counts,
verified: true,
+1 -1
View File
@@ -303,7 +303,7 @@ const workflowAvailabilityFor = ({
: {
reason: "provider_missing",
remediation:
"On Linux x64, provide an absolute REA_WAKARU_COMMAND for Wakaru 1.13.0 and util-linux prlimit. No binary target is required.",
"On Linux x64, provide an absolute REA_WAKARU_COMMAND for Wakaru ^1.13.0 (verified with 1.14.0) and util-linux prlimit; see docs/javascript-recovery.md#install-wakaru. No binary target is required.",
};
if (name === "inspect_binary_layout")
return policy.binaryLayoutEnabled === true
@@ -13,7 +13,7 @@ export const JAVASCRIPT_RECOVERY_TOOL_CONTRACTS = [
...toolContractMetadata("recover_javascript_sources"),
kind: "application",
description:
"Recover readable modules from one local UTF-8 JavaScript script/bundle into an absent directory, using caller-supplied Wakaru 1.13.0 on Linux x64. Returns original/derived digests, extraction byte ranges, emitted source maps, warnings and analysis_input inline. Structural splitting may fall back to one module; original names and runtime equivalence remain unknown. No application code execution. Requires util-linux prlimit; one worker, 1 GiB address-space ceiling, 120-second deadline, 64 MiB input, 128 MiB output, 10000 entries and 8 MiB per diagnostic/report stream.",
"Recover readable modules from one local UTF-8 JavaScript script/bundle into an absent directory, using caller-supplied Wakaru ^1.13.0 (verified with 1.14.0) on Linux x64. Returns original/derived digests, extraction byte ranges, emitted source maps, warnings and analysis_input inline. Structural splitting may fall back to one module; original names and runtime equivalence remain unknown. No application code execution. Requires util-linux prlimit; one worker, 1 GiB address-space ceiling, 120-second deadline, 64 MiB input, 128 MiB output, 10000 entries and 8 MiB per diagnostic/report stream.",
inputSchema: javascriptRecoveryInputSchema,
outputSchema: evidenceResultOf(javascriptRecoveryResultSchema),
examples: [
+1 -1
View File
@@ -26,7 +26,7 @@ import { RECOVERY_LIMITS } from "./WakaruRelease.js";
const OPERATION = "recover_javascript_sources";
const WAKARU_REQUIREMENT =
"Provide an absolute REA_WAKARU_COMMAND pointing to Wakaru 1.13.0 on Linux x64; no tool is installed by REA";
"Provide an absolute REA_WAKARU_COMMAND pointing to a Wakaru ^1.13.0 Linux x64 binary (verified with 1.14.0); no tool is installed by REA. See docs/javascript-recovery.md#install-wakaru.";
/** Inject the owned process launcher while retaining production protocol parsing. */
export type WakaruLauncher = (
+36 -32
View File
@@ -24,11 +24,8 @@ import {
} from "./WakaruCommand.js";
import { parseWakaruReports } from "./WakaruReport.js";
import { publishWakaruArtifacts } from "./WakaruPublication.js";
import {
RECOVERY_LIMITS,
WAKARU_RELEASE,
WAKARU_PROVIDER_IDENTITY,
} from "./WakaruRelease.js";
import { RECOVERY_LIMITS, WAKARU_PROVIDER_IDENTITY } from "./WakaruRelease.js";
import { admitWakaruVersion } from "./WakaruVersion.js";
const OPERATION = "recover_javascript_sources";
const MODES = {
@@ -66,12 +63,13 @@ const executeWakaruRecovery = async (context: RecoveryWorkspace) => {
...commandContext,
arguments: ["--version"],
});
if (
version.exit_code !== 0 ||
version.stdout.trim() !== `wakaru ${WAKARU_RELEASE.version}`
) {
const admitted =
version.exit_code === 0
? admitWakaruVersion(version.stdout.trim())
: undefined;
if (admitted === undefined || admitted.status === "unsupported") {
// The message names the mismatch; the reason also keeps the tool's stderr.
const mismatch = `This adapter requires Wakaru ${WAKARU_RELEASE.version}; reported version: ${version.stdout.trim()}; exit: ${String(version.exit_code)}`;
const mismatch = `${admitted?.message ?? `Wakaru --version failed; reported version: ${version.stdout.trim()}`}; exit: ${String(version.exit_code)}`;
throw new AnalysisCapabilityUnavailableError(
"wakaru",
OPERATION,
@@ -106,20 +104,22 @@ const executeWakaruRecovery = async (context: RecoveryWorkspace) => {
sourceBytes: snapshot,
files,
});
return { version, run, parsed };
return { version, admitted, run, parsed };
};
/** Publish validated recovery and retain exact source and engine bindings. */
export const prepareWakaruExecution = async (context: RecoveryWorkspace) => {
const { root, tree, input, source, engine, options } = context;
const staging = join(root, "modules");
const { version, run, parsed } = await executeWakaruRecovery(context);
const { version, admitted, run, parsed } =
await executeWakaruRecovery(context);
const result = await publishWakaruArtifacts({
tree,
staging,
input,
source,
engine,
admitted,
parsed,
execution: run,
...(options?.signal === undefined ? {} : { signal: options.signal }),
@@ -150,25 +150,29 @@ export const prepareWakaruExecution = async (context: RecoveryWorkspace) => {
OPERATION,
"Configured engine bytes changed during recovery",
);
return createAnalysisExecution(result, WAKARU_PROVIDER_IDENTITY, {
rawResult: jsonValueSchema.parse({
version,
execution: run,
report: parsed.report,
provenance: parsed.provenance,
}),
subject: {
path: source.path,
sha256: source.sha256,
format: "javascript",
return createAnalysisExecution(
result,
{ ...WAKARU_PROVIDER_IDENTITY, version: admitted.version },
{
rawResult: jsonValueSchema.parse({
version,
execution: run,
report: parsed.report,
provenance: parsed.provenance,
}),
subject: {
path: source.path,
sha256: source.sha256,
format: "javascript",
},
locations: [
{ kind: "artifact-path", path: source.path },
...result.modules.map((module) => ({
kind: "artifact-path" as const,
path: module.artifact.path,
})),
],
limitations: result.limitations,
},
locations: [
{ kind: "artifact-path", path: source.path },
...result.modules.map((module) => ({
kind: "artifact-path" as const,
path: module.artifact.path,
})),
],
limitations: result.limitations,
});
);
};
+17 -3
View File
@@ -22,6 +22,7 @@ import type {
resolveWakaruCommand,
runWakaruCommand,
} from "./WakaruCommand.js";
import type { WakaruVersionAdmission } from "./WakaruVersion.js";
type PublicationContext = {
tree: SafeOutputTree;
@@ -34,6 +35,7 @@ type PublicationContext = {
bytes: number;
};
engine: Awaited<ReturnType<typeof resolveWakaruCommand>>;
admitted: Exclude<WakaruVersionAdmission, { status: "unsupported" }>;
parsed: Awaited<ReturnType<typeof parseWakaruReports>>;
execution: Awaited<ReturnType<typeof runWakaruCommand>>;
signal?: AbortSignal;
@@ -86,7 +88,7 @@ export const publishWakaruArtifacts = async (
source: { ...source, published_copy: publishedCopy },
engine: {
id: "wakaru",
version: WAKARU_RELEASE.version,
version: context.admitted.version,
executable: {
path: context.engine.command,
sha256: context.engine.sha256,
@@ -107,7 +109,14 @@ export const publishWakaruArtifacts = async (
total: parsed.report.total,
failed: parsed.report.failed,
modules,
warnings: parsed.report.warnings,
warnings: parsed.report.warnings.map(
({ filename, kind, is_error, message }) => ({
filename,
kind,
is_error,
message,
}),
),
report,
provenance,
analysis_input:
@@ -118,7 +127,12 @@ export const publishWakaruArtifacts = async (
format: "directory" as const,
},
runtime_equivalence: "unknown" as const,
limitations: [...RECOVERY_LIMITATIONS],
limitations: [
...RECOVERY_LIMITATIONS,
...(context.admitted.status === "compatible"
? [context.admitted.limitation]
: []),
],
};
const manifest = await write(
"manifest.json",
+11 -3
View File
@@ -1,11 +1,19 @@
/** Unchanged upstream source pin; executed bytes are identified separately. */
export const WAKARU_RELEASE = {
version: "1.13.0",
revision: "6070266d24b32951fa2fe1d2dad5b4313bd5c81b",
version: "1.14.0",
revision: "8219bf5016a063d7fb215101a8c45d5959ec733f",
repository: "https://github.com/pionxzh/wakaru",
} as const;
/** Exact provider identity shared by executions and the generated catalog. */
/**
* Releases the report parser accepts. Wakaru changes `--version`, `--json`
* and `provenance.json` only by addition within a major version
* (https://github.com/pionxzh/wakaru/blob/v1.14.0/docs/cli.md#machine-readable-output-compatibility);
* 1.13.0 is the first release this adapter verified.
*/
export const WAKARU_ACCEPTED_RANGE = "^1.13.0";
/** Catalog identity for the verified Wakaru release. Evidence replaces version with the observed banner. */
export const WAKARU_PROVIDER_IDENTITY = {
id: "wakaru",
name: "Wakaru",
+12 -7
View File
@@ -10,20 +10,20 @@ import { readRecoveryFile, recoveryFailureMessage } from "./RecoveryFiles.js";
import { RECOVERY_LIMITS } from "./WakaruRelease.js";
import type { runWakaruCommand } from "./WakaruCommand.js";
const warningSchema = z.strictObject({
const warningSchema = z.looseObject({
filename: z.string(),
kind: z.string(),
is_error: z.boolean(),
message: z.string(),
});
const reportSchema = z.strictObject({
const reportSchema = z.looseObject({
detected_formats: z.array(z.string()),
safety: z.string(),
modules: z.array(
z.strictObject({
z.looseObject({
filename: z.string().min(1),
kind: z.literal("javascript"),
status: z.literal("decompiled"),
status: z.string().min(1),
}),
),
warnings: z.array(warningSchema),
@@ -35,12 +35,12 @@ const rangeSchema = z.tuple([
z.number().int().nonnegative(),
z.number().int().nonnegative(),
]);
const provenanceSchema = z.strictObject({
const provenanceSchema = z.looseObject({
format: z.string(),
strategy: z.string(),
modules: z.record(
z.string(),
z.strictObject({
z.looseObject({
input: z.string(),
ranges: z.array(rangeSchema),
extraction: z.string(),
@@ -59,7 +59,12 @@ type ReportContext = {
files: readonly { relativePath: string }[];
};
/** Parse exact pinned producer JSON before projecting provider-neutral artifacts. */
/**
* Parse producer JSON before projecting provider-neutral artifacts. Wakaru adds
* fields and category values within a major version, so unknown keys are kept
* and `status` stays the reported string; identity, path, range and file-set
* checks below remain exact.
*/
export const parseWakaruReports = async (context: ReportContext) => {
const { execution, provenancePath } = context;
@@ -0,0 +1,39 @@
import { describe, expect, it } from "vitest";
import { admitWakaruVersion } from "./WakaruVersion.js";
import { WAKARU_RELEASE } from "./WakaruRelease.js";
describe("Wakaru release range", () => {
it("accepts the verified banner without an extra limitation", () => {
expect(admitWakaruVersion(`wakaru ${WAKARU_RELEASE.version}`)).toEqual({
status: "verified",
version: WAKARU_RELEASE.version,
});
});
it.each([
["wakaru 1.13.0", "1.13.0"],
["wakaru 1.15.2", "1.15.2"],
])("accepts %s as compatible", (banner, version) => {
const admitted = admitWakaruVersion(banner);
expect(admitted).toMatchObject({ status: "compatible", version });
if (admitted.status !== "compatible")
throw new Error("expected a compatible release");
expect(admitted.limitation).toContain(version);
expect(admitted.limitation).toContain(WAKARU_RELEASE.version);
});
it.each([
["wakaru 1.12.0", "wakaru 1.12.0"],
["wakaru 2.0.0", "wakaru 2.0.0"],
["wakaru 1.15.0-rc.1", "wakaru 1.15.0-rc.1"],
["wakaru v1.14.0", "wakaru v1.14.0"],
["1.14.0", "1.14.0"],
["", "missing"],
])("rejects banner %j", (banner, detail) => {
const admitted = admitWakaruVersion(banner);
if (admitted.status !== "unsupported")
throw new Error("expected a rejected banner");
expect(admitted.message).toContain(detail);
expect(admitted.message).toContain("^1.13.0");
});
});
+39
View File
@@ -0,0 +1,39 @@
import { satisfies, valid } from "semver";
import { WAKARU_ACCEPTED_RANGE, WAKARU_RELEASE } from "./WakaruRelease.js";
/** Admission of one observed `wakaru --version` banner. */
export type WakaruVersionAdmission =
| { readonly status: "verified"; readonly version: string }
| {
readonly status: "compatible";
readonly version: string;
readonly limitation: string;
}
| { readonly status: "unsupported"; readonly message: string };
/** Caller-facing accepted releases, naming the verified build. */
export const wakaruReleaseLine = (): string =>
`Wakaru ${WAKARU_ACCEPTED_RANGE} (verified with ${WAKARU_RELEASE.version})`;
/** Classify a `wakaru <version>` banner against the accepted release range. */
export const admitWakaruVersion = (banner: string): WakaruVersionAdmission => {
const token = banner.startsWith("wakaru ")
? banner.slice("wakaru ".length)
: undefined;
if (
token === undefined ||
valid(token) !== token ||
!satisfies(token, WAKARU_ACCEPTED_RANGE)
)
return {
status: "unsupported",
message: `Unsupported Wakaru version ${banner.length === 0 ? "missing" : banner}; accepted releases are ${wakaruReleaseLine()}`,
};
if (token === WAKARU_RELEASE.version)
return { status: "verified", version: token };
return {
status: "compatible",
version: token,
limitation: `This session uses Wakaru ${token}. The report parser is verified against ${WAKARU_RELEASE.version}; other ${WAKARU_ACCEPTED_RANGE} releases are accepted and the observed version is reported.`,
};
};
@@ -97,6 +97,44 @@ describe.skipIf(process.platform !== "linux" || process.arch !== "x64")(
await assertRecoveryCleanup(fixture.launches);
});
it("accepts a later release on the verified major line with additive report fields", async () => {
const fixture = await recoveryFixture("additive");
const result = await fixture.service.recover({
path: fixture.path,
output_directory: fixture.output,
});
if (!result.ok) throw result.error;
const value = javascriptRecoveryResultSchema.parse(
result.value.normalized_result,
);
expect(value).toMatchObject({
status: "complete",
engine: { version: "1.99.0" },
modules: [{ reported_status: "future_status" }],
});
expect(result.value.provider.version).toBe("1.99.0");
expect(
value.limitations.some((limitation) =>
limitation.includes("This session uses Wakaru 1.99.0"),
),
).toBe(true);
expect(
JSON.parse(await readFile(value.report.path, "utf8")),
).toMatchObject({
future_report_field: { nested: true },
warnings: [{ code: "W001" }],
});
expect(value.warnings).toEqual([
{
filename: "module.js",
kind: "notice",
is_error: false,
message: "future warning",
},
]);
await assertRecoveryCleanup(fixture.launches);
});
it("retains valid partial output and exact failure diagnostics", async () => {
const fixture = await recoveryFixture("partial");
const result = await fixture.service.recover({
+33 -3
View File
@@ -4,7 +4,13 @@ import { dirname, join } from "node:path";
const args = process.argv.slice(2);
const mode = process.env.REA_RECOVERY_FIXTURE_MODE ?? "normal";
if (args.includes("--version")) {
console.log(mode === "wrong-version" ? "wakaru 0.0.0" : "wakaru 1.13.0");
console.log(
mode === "wrong-version"
? "wakaru 0.0.0"
: mode === "additive"
? "wakaru 1.99.0"
: "wakaru 1.14.0",
);
} else {
const input = args[0];
const output = args[args.indexOf("-o") + 1];
@@ -25,7 +31,16 @@ if (args.includes("--version")) {
if (mode === "source-change")
await writeFile(process.env.REA_RECOVERY_ORIGINAL_PATH, "changed original");
const filename = mode === "escaping" ? "../escaped.js" : "module.js";
const modules = [{ filename, kind: "javascript", status: "decompiled" }];
const modules = [
mode === "additive"
? {
filename,
kind: "javascript",
status: "future_status",
future_module_field: true,
}
: { filename, kind: "javascript", status: "decompiled" },
];
if (mode === "duplicate") modules.push(modules[0]);
if (mode !== "missing-file" && mode !== "escaping") {
if (mode === "symlink") await symlink(input, join(output, filename));
@@ -65,6 +80,10 @@ if (args.includes("--version")) {
},
},
};
if (mode === "additive") {
provenance.future_top_field = "added";
provenance.modules[filename].future_entry_field = [1];
}
if (mode === "extra-provenance")
provenance.modules["extra.js"] = provenance.modules[filename];
if (mode !== "missing-provenance")
@@ -86,7 +105,18 @@ if (args.includes("--version")) {
message: "fixture transform failed; retained source",
},
]
: [],
: mode === "additive"
? [
{
filename,
kind: "notice",
is_error: false,
message: "future warning",
code: "W001",
},
]
: [],
...(mode === "additive" ? { future_report_field: { nested: true } } : {}),
total: modules.length,
failed: mode === "partial" ? 1 : 0,
elapsed_ms: 1,