Skip to content
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
546119b
feat(artifacts): inventory and extract ESP32 firmware images
Entrpi Oct 27, 2026
38894a8
test(artifacts): run the ESP image lane on esptool v5.6.0
Entrpi Oct 27, 2026
480676f
refactor(artifacts): define ESP image layout sizes once
Entrpi Oct 27, 2026
0c44224
fix(artifacts): report ESP-IDF's segment length limit by chip
Entrpi Oct 27, 2026
f4b8ef4
test(artifacts): check ESP lane tools first and verify extraction
Entrpi Oct 27, 2026
2bdab09
docs(artifacts): state what the ESP image lane does not establish
Entrpi Oct 27, 2026
8201d88
fix(artifacts): compare ESP segment declarations
Entrpi Oct 27, 2026
a81b901
test(artifacts): run ESP lane commands as owned process trees
Entrpi Oct 27, 2026
c089978
test(artifacts): cancel ESP decoding after its first decoded chunk
Entrpi Oct 27, 2026
ea2b386
docs(artifacts): record which ESP metadata comparison ignores
Entrpi Oct 27, 2026
9e65be9
fix(artifacts): decide declaration comparison for every metadata kind
Entrpi Oct 27, 2026
8886971
fix(artifacts): keep content comparison for metadata records with con…
Entrpi Oct 27, 2026
2b66531
fix(artifacts): compare encryption changes when content is unavailable
Entrpi Oct 27, 2026
2fc3bd2
fix(artifacts): compare entry kind and executable when content is una…
Entrpi Oct 27, 2026
a6e09ec
fix(artifacts): never mark a symlink executable
Entrpi Oct 27, 2026
9dbf90b
fix(artifacts): stop comparing limitation wording
Entrpi Oct 27, 2026
0c92f1c
fix(artifacts): compare declared digests when content is unavailable
Entrpi Oct 27, 2026
1a068ac
docs(artifacts): check the contract's compared kinds against the table
Entrpi Oct 28, 2026
964be74
test(artifacts): share the comparison acceptance MCP fixture
Entrpi Oct 28, 2026
8c3010e
Merge upstream main into fix-artifacts-metadata-comparison
Entrpi Oct 29, 2026
99f01ae
Merge fix-artifacts-metadata-comparison into fix-artifacts-encryption…
Entrpi Oct 29, 2026
a9ed089
Merge fix-artifacts-encryption-comparison into fix-artifacts-comparison
Entrpi Oct 29, 2026
1ffe4c0
Merge upstream main into fix-artifacts-metadata-comparison
Entrpi Oct 29, 2026
3e51a77
fix(artifacts): leave .NET bundle member declarations uncompared
Entrpi Oct 29, 2026
9e580a6
Merge fix-artifacts-metadata-comparison into fix-artifacts-encryption…
Entrpi Oct 29, 2026
956477c
Merge fix-artifacts-encryption-comparison into fix-artifacts-comparison
Entrpi Oct 29, 2026
2d54fcf
Merge upstream main into fix-artifacts-metadata-comparison
Entrpi Oct 30, 2026
7978b94
fix(artifacts): leave HEX region declarations uncompared
Entrpi Oct 30, 2026
9ca54d6
test(artifacts): compare node-less records of any compared kind
Entrpi Oct 30, 2026
f68a4f7
Merge fix-artifacts-metadata-comparison into fix-artifacts-encryption…
Entrpi Oct 30, 2026
eb46be7
test(artifacts): compare a node-less record's recorded ASAR storage
Entrpi Oct 30, 2026
2304a05
Merge fix-artifacts-encryption-comparison into fix-artifacts-comparison
Entrpi Oct 30, 2026
9d5e31a
test(artifacts): compare UnityFS member declarations through MCP
Entrpi Oct 30, 2026
b6adeb9
Merge fix-artifacts-metadata-comparison into fix-artifacts-encryption…
Entrpi Oct 30, 2026
33d963e
Merge fix-artifacts-encryption-comparison into fix-artifacts-comparison
Entrpi Oct 30, 2026
8177a12
test(artifacts): use the shared comparison fixture for UnityFS
Entrpi Oct 30, 2026
6b59c4f
test(artifacts): find the contract's compared kinds by marker
Entrpi Oct 30, 2026
c0ed340
Merge upstream main into fix-artifacts-comparison
Entrpi Oct 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions docs/godot-pck.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,33 @@ by default; `--integrity-policy record-and-continue` retains the observed bytes
and an explicit untrusted contradiction. An all-zero MD5 is an unknown claim,
not a verified digest.

Artifact comparison pairs members by `logical_path`, the stored path without
`res://`, and compares their bytes and occurrence facts. It does not compare
`godot-pck-entry` metadata. Godot 4.3
[stores `res://` paths](https://github.com/godotengine/godot/blob/77dcf97d82cbfe4e4615475fa52ca03da645dbd8/editor/export/editor_export_platform.cpp#L228)
and [keys members by them](https://github.com/godotengine/godot/blob/77dcf97d82cbfe4e4615475fa52ca03da645dbd8/core/io/file_access_pack.cpp#L50-L52).
Godot 4.4.1
[stores paths without the prefix](https://github.com/godotengine/godot/blob/49a5bc7b616bd04689a2c89e89bda41f50241464/editor/export/editor_export_platform.cpp#L257-L260)
and [removes it before keying](https://github.com/godotengine/godot/blob/49a5bc7b616bd04689a2c89e89bda41f50241464/core/io/file_access_pack.cpp#L48-L50).
Each version finds `res://scenes/main.gd` in its own export, but comparing the
stored `source_path` would mark every member changed between such exports.
`logical_path` names the member Godot's lookup finds, except in two cases.
REA normalizes Unicode to NFC, so a member renamed only between Unicode forms
compares unchanged, although Godot treats it as another path. Godot reads `\`
as `/` and REA keeps it literal, so a member spelled with backslashes compares
as removed and added. Directory order and offsets are layout. The encryption
bit, the only member flag REA admits, is the occurrence's `encrypted` field. A
member whose encryption changes compares `unknown` in `metadata` and
`availability`: the change is known, but REA does not read encrypted bytes,
so whether the script changed is unknown. Godot's exporter
[stores the MD5 of a member's original bytes](https://github.com/godotengine/godot/blob/a13da4feb8d8aefc283c3763d33a2f170a18d541/editor/export/editor_export_platform.cpp#L465-L468),
as the 4.3 and 4.4.1 exporters do, so the declared MD5 does not depend on the
encryption IV. Encrypted members whose declared MD5s differ therefore also
compare `unknown` in `metadata` and `availability`: the MD5s declare
different scripts, but REA cannot verify them, and equal MD5s add nothing for
the same reason. An encrypted member with an MD5 on only one side stays
`unknown`.

## CLI

Start with a compact summary and keep the complete Evidence bundle:
Expand Down
5 changes: 4 additions & 1 deletion docs/hex-records.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ regions and are never filled.

Region `format_metadata` (`hex-record-region`) records `start_address`,
`end_address`, `length`, `record_count`, and `first_line`/`last_line`: the
1-based line span of the contributing data records.
1-based line span of the contributing data records. `compare_artifacts`
compares none of these fields: a region's name encodes its start and end
addresses, its length follows from them, and record and line counts are
layout.

Root `format_metadata` (`hex-records`) records:

Expand Down
37 changes: 37 additions & 0 deletions docs/mcp-contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -1338,6 +1338,43 @@ support for omitted historical values.
known and differ. If only one value is known, storage comparison is unknown;
when both are omitted, storage is not compared.

`compare_artifacts` also compares the declarations in the `format_metadata`
of UnityFS members (`unityfs-entry`), PyInstaller entries
(`pyinstaller-entry`), Node SEA members (`node-sea-entry`),
ESP image segments (`esp-image-segment`) and FIT images (`fit-image`); each
format's guide names the compared fields. A changed declaration is a metadata
change, including when content is unavailable. A declaration that only one
side records, or that the other side records as another kind, is unknown and
adds a limitation. A field that either side could not read is unknown, and
the other fields still compare. Other kinds are not compared: their fields
record layout, come from the bytes of the artifact or its container, or
repeat occurrence fields, except Godot PCK source paths, which
[Godot PCK archives](godot-pck.md) explains.

Every occurrence records `entry_kind`, `executable` and `encrypted`. A
different entry kind, `encrypted` value or occurrence limitation text is a
`metadata` difference, and a different `executable` value is an `executable`
difference. A symlink's own permission bits never mark it `executable`,
because running a symlink runs its target. When either side's content is
unavailable, `compare_artifacts` lists the differences that it can still
observe with `availability`, and the path compares `unknown` unless a
declaration changed. REA does not read symlink targets or encrypted bytes,
so a non-executable file that becomes a symlink compares `unknown` in
`metadata` and `availability`, and so does a member that becomes encrypted.
Declared sizes do not count without content: REA compares observed sizes.
For a node-less metadata record with a compared declaration, only that
declaration, these fields and any recorded ASAR storage decide the
comparison, so a record that becomes encrypted compares `changed` in
`metadata`. Such a record adds `availability` only when a declaration is
unknown. REA does not compare the record's unread payload or its
limitations, which describe the observation.

When either side has no content, `compare_artifacts` also compares the
occurrences' `declared_integrity` claims, which Godot PCK and ASAR readers
record. Claims of one algorithm that differ add `metadata`. REA cannot verify
them, so they do not make the path `changed`, and equal claims add nothing.
With content on both sides, the observed bytes decide.

POSIX letter-colon names such as `a:b` remain valid logical
names; Windows extraction refuses names its filesystem cannot materialize.

Expand Down
19 changes: 11 additions & 8 deletions docs/python-packaged-applications.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,17 @@ table index; original names remain available in the supplied artifact.
Path failures use bounded escaped previews and report the original length;
accepted paths and retained source names are not shortened.

`compare_artifacts` compares these node-less records as declarations using the
container, container name, source name and type code. Equal option or dependency
declarations can therefore compare unchanged without implying that an external
dependency's bytes were found. For materialized PyInstaller members, the same
fields participate in the metadata delta independently of content identity;
archive offsets and table positions remain location facts, not declaration
identity. Missing declaration facts and unavailable non-metadata content remain
unknown.
`compare_artifacts` compares node-less records, namely runtime options,
dependencies and namespace packages, as declarations using the container,
container name, source name and type code, and whether the record is
encrypted; their limitations are not compared. Equal declarations can
therefore compare unchanged without implying that an external dependency's
bytes were found or that unread legacy namespace payloads match. For
materialized PyInstaller members, the same fields participate in the metadata
delta independently of content identity, including when the member's bytes
are unavailable; archive offsets and table positions remain location facts,
not declaration identity. Missing declaration facts remain unknown, as does
unavailable non-metadata content whose declaration is unchanged.

Request `detail: "summary"` for a compact inspection, then query retained
Evidence with `inspect_analysis_view`. An inventory view can select
Expand Down
4 changes: 3 additions & 1 deletion docs/unityfs.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ strings, declared size, dataflags, blocks-and-directory table bounds, the
producer's 16-byte data hash as declared, and the resolved data-area offset.
Members (`CAB-*` SerializedFiles, `.resS` resource streams, shared assets)
retain declared source paths, virtual stream offsets, sizes and raw node flags.
The directory flag identifies directories. Virtual offsets are not physical
The directory flag identifies directories. `compare_artifacts` compares each
member's declared `source_path` and raw `flags`; its index and offset are
layout. Virtual offsets are not physical
carrier locations: member `source_location` is null. Member reads only examine
their covering blocks. Uncompressed ranges stream directly; compressed ranges
reuse at most one completed decoded block. Independent 64 KiB output chunks
Expand Down
28 changes: 26 additions & 2 deletions src/artifacts/hostExecutableMarking.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,21 @@ import { describe, expect, it } from "vitest";

import { hostExecutableMarking } from "./hostExecutableMarking.js";

const file = (mode: number) => ({ mode, isFile: () => true });
const directory = (mode: number) => ({ mode, isFile: () => false });
const file = (mode: number) => ({
mode,
isFile: () => true,
isSymbolicLink: () => false,
});
const directory = (mode: number) => ({
mode,
isFile: () => false,
isSymbolicLink: () => false,
});
const symlink = (mode: number) => ({
mode,
isFile: () => false,
isSymbolicLink: () => true,
});

describe("hostExecutableMarking", () => {
it("marks Windows launchable files by extension because NTFS has no execute bit", () => {
Expand Down Expand Up @@ -48,4 +61,15 @@ describe("hostExecutableMarking", () => {
hostExecutableMarking("/scratch/whoami.exe", file(0o644), "darwin"),
).toBe(false);
});

it("never marks a symlink, whose target decides what runs", () => {
for (const [path, platform] of [
["/scratch/current", "linux"],
["/scratch/current", "darwin"],
["C:\\scratch\\tool.exe", "win32"],
] as const)
expect(hostExecutableMarking(path, symlink(0o120777), platform)).toBe(
false,
);
});
});
20 changes: 12 additions & 8 deletions src/artifacts/hostExecutableMarking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,20 @@ const windowsLaunchableExtensions = new Set([".bat", ".cmd", ".com", ".exe"]);
*
* POSIX hosts mark executability with permission bits. Windows has no execute
* bit (Node reports file modes from the read-only attribute alone), so it marks
* launchable files by extension instead.
* launchable files by extension instead. A symlink carries no marking on any
* host: running it runs its target, and its own permission bits do not
* control that (Linux reports 0777 for every symlink).
*/
export const hostExecutableMarking = (
path: string,
metadata: Pick<Stats | BigIntStats, "mode" | "isFile">,
metadata: Pick<Stats | BigIntStats, "mode" | "isFile" | "isSymbolicLink">,
platform: NodeJS.Platform = process.platform,
): boolean =>
platform === "win32"
? metadata.isFile() &&
windowsLaunchableExtensions.has(extname(path).toLowerCase())
: typeof metadata.mode === "bigint"
? (metadata.mode & 0o111n) !== 0n
: (metadata.mode & 0o111) !== 0;
metadata.isSymbolicLink()
? false
: platform === "win32"
? metadata.isFile() &&
windowsLaunchableExtensions.has(extname(path).toLowerCase())
: typeof metadata.mode === "bigint"
? (metadata.mode & 0o111n) !== 0n
: (metadata.mode & 0o111) !== 0;
Loading
Loading