Compare commits

...
Author SHA1 Message Date
Kaoru0822andClaude Opus 5.5 4eaf0aae73 docs(browser): fence the dry-run plan example as output text
Every JSON block in the scenario contract guide is admitted as a scenario
by the product catalog test; the plan is output, not input.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 10:14:57 +00:00
Kaoru0822andClaude Opus 5.5 c3b79cebe6 feat(browser): validate browser scenarios with --dry-run before launching
Checking a scenario file meant running it, which discovers, launches or
attaches to a browser and can change page state before a typo surfaces.

`rea capture-browser-scenario <json> --dry-run` reuses the same JSON and
path resolution, the canonical scenario schema and its semantic rules, and
the secret-availability rule capture applies before acquiring a browser,
now shared from one domain owner. It returns the same projected input
errors, or a compact plan of steps, secret availability and the stages it
did not check, and never creates a provider or reads secret values.

Fixes #5488

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 09:58:02 +00:00
morluto bc143125ed fix(release): reject missing manifest release boundaries 2026-10-25 17:25:18 +08:00
github-actions[bot]andmorluto 160d821ac9 chore(main): release rea-agents 6.8.0 (#5004)
* chore(main): release rea-agents 6.8.0

* docs(release): finalize contributor-aware 6.8.0 notes

* docs(release): pin the JSON Lines migration guide

* docs(release): cover the remaining reviewed user-facing fixes

* docs(release): refresh the reviewed contribution snapshot

* docs(release): correct migration references and gateway scope

Preserve the concurrent 6.8.0 editorial draft and verified credits. Cite the actual keyed-archive UID migration, move CLI version and superclass-classification references to their matching entries, retain unknown-date plist behavior, and avoid implying live gateway acceptance. Existing author readiness declarations remain unchanged.

* docs(release): include nine newly merged changes in 6.8.0

* docs(release): include recorded write views in 6.8.0

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-25 16:35:15 +08:00
113679c9a6 feat(environment): serve creates whose answers show less than reads, and whole-object updates (#7080)
* feat(environment): take created entities' read-backs and re-derived update members

- A create's observation takes the fields the first read of its entity
  shows that the answer lacks, and an item read's clock spellings, when the
  read agrees with the answer otherwise. The plan records the read
  (observation.read with its shape); an item read then observes rather than
  checks, a list read only adds fields and still checks. Serving merges the
  same recorded objects.
- An update member sent back as the clock the entity held and stored as a
  fresh clock near the call is a clock rule; one stored as the slug of
  another member stored as sent is a slug rule. Both are accepted with any
  value, as whole-object writes send what they read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(environment): record the write-views verification

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(environment): reconcile write views without hiding contradictions

* fix(environment): reject incompatible slug-only saves

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-25 15:06:27 +08:00
Adolanium 80673b22a4 fix(javascript): report fork's silent stdio as piped (#7083)
* fix(javascript): report fork's silent stdio as piped

Node's child_process.fork pipes stdin, stdout and stderr when options.silent is truthy and options.stdio is neither a string nor an array, and inherits them otherwise. REA never read silent, so fork("worker.js", { silent: true }) was reported with stdio_mode default (inherited). The unset-stdio case of fork now reads silent: truthy gives pipe, falsy or missing gives default, and an unresolved value gives dynamic.

* fix(javascript): apply fork's silent rule to other known stdio values
2026-10-25 14:23:46 +08:00
Adolanium c74d302cc4 fix(ghidra): report a DOL language override conflict as a configuration error (#7081)
* fix(ghidra): report a DOL language override conflict as a configuration error

A REA_GHIDRA_LANGUAGE_ID outside PowerPC:BE:32:* made an otherwise admitted GameCube DOL an unsupported target. docs/ghidra-provider.md and the other fixed-import lanes treat such an override as a configuration error naming the conflicting setting. The DOL lane now uses the same languageOverrideConflict owner and names only REA_GHIDRA_LANGUAGE_ID, since DOL accepts a compiler-spec override.

* test(ghidra): keep a valid compiler spec out of the DOL language conflict
2026-10-25 14:19:14 +08:00
2c38becc4e feat(binary-layout): admit ELF32 i386, ARM and MIPS layout profiles (#7052)
* feat(binary-layout): admit ELF32 i386, ARM and MIPS layout profiles

inspect_binary_layout refused every ELF32 object, so router and IoT
firmware (ARM32, MIPS32) got no layout facts at all.

The pwntools adapter now admits EXEC/DYN/REL objects in these profiles:
ELF64 x86-64, ELF32 i386, ELF32 little-endian ARM and ELF32 little- or
big-endian MIPS. Header fields are read in the declared byte order, the
ELF32 header is bounds-checked at 52 bytes, and pwntools' own reported
architecture, class and byte order must match the admitted profile.
Other machines, classes and byte orders keep an unsupported_target
refusal that names the admitted profiles.

The output schema admits exactly those architecture combinations, and
the Evidence subject now names the reported machine instead of always
claiming x86_64.

verify:binary:layout cross-compiles i386, ARM, MIPS and MIPSel
executables and relocatable objects (never executed) and checks every
section name, address, offset and size against GNU readelf through CLI
and MCP. The real-binary-layout workflow installs those toolchains.

Refs #6934

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* ci(binary-layout): install target libc headers for ELF32 fixtures

--no-install-recommends skips the libc6-dev-*-cross packages the cross
compilers only recommend, so the i686 fixture fell back to the host's
/usr/include and failed to find bits/libc-header-start.h. Install the
four target libc development packages explicitly.

Refs #6934

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(binary-layout): size the minimum symbol entry by ELF class

The relocation reference check required a 24-byte Elf64_Sym entry, so
every positive symbol reference in an ELF32 report was refused as
unreadable output. Elf32_Sym is 16 bytes; take the minimum from the
reported class. A 64-bit table with 16-byte entries is still refused.

Refs #6934

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(binary-layout): compare ELF32 symbols and relocations with readelf

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-25 14:18:44 +08:00
rodricksz4h5 0a2b7af905 fix(apple): keep the documented terminating process, parent pid and metadata members of IPS reports (#7079)
Apple's crash report JSON documents termination.byPid, byProc and flags, the body's parentPid, and
the metadata line's build_version and platform. The parser shapes did not list them, so they were
reported only by name in unlisted_keys / unlisted_body_keys and their values were dropped: a
watchdog kill no longer said which process ended it, and process kept parentProc without its pid.
#4297 asks for termination members beyond namespace/code/indicator to be retained as recorded, and
docs/apple-ips.md says documented fields are reproduced.

They are now kept: termination.by_proc, termination.by_pid and termination.flags, process.parent_pid
and metadata.build_version / metadata.platform. The integers are recorded integers, so they count
toward max_integer_text_bytes and a type mismatch is recorded like the other fields.
2026-10-25 13:56:51 +08:00
RRFZ cc146042d1 fix(cli): preserve literal Markdown table facts (#7073)
* fix(cli): preserve literal Markdown table facts

* test(cli): retain capacity diagnostics through Markdown escaping

* fix(cli): keep intraword underscores readable in Markdown tables

Underscore runs between letters or digits cannot open or close CommonMark emphasis, so escaping them only adds noise to identifiers such as source_index and invalid_request in raw Markdown. Escape underscore runs only at word boundaries and restore the original diagnostic assertions.
2026-10-25 13:55:18 +08:00
Wenyu Chiou fead4fdaa7 fix(artifacts): preserve integrity contradictions for one-sided members (#7076) 2026-10-25 13:47:41 +08:00
Adolanium 4973ce7202 fix(capture): report an out-of-range record ordinal as a selection fault (#7067)
inspect_web_network_capture reported a record_ordinals entry past the capture's records as an invalid_format issue on record_ordinals, with no bounds. It is now an out_of_range issue on that entry (record_ordinals[i]) with minimum 0 and maximum total_records - 1, or invalid_value when the capture has no records.
2026-10-25 13:46:26 +08:00
Adolanium 06a4c338fc fix(protobuf): exclude trailing whitespace from gRPC-web trailer values (#7069)
RFC 9110 section 5.5 excludes leading and trailing whitespace from a field value. The gRPC-web trailer parser stripped only leading SP/HTAB, so 'grpc-status: 0 ' was reported as '0 '. Trailing SP/HTAB is now excluded too; internal whitespace and the examined byte count are unchanged.
2026-10-25 13:45:22 +08:00
Adolanium 60ab148afa fix(ghidra): accept stock language IDs with LEBE endianness or a slash (#7070)
REA_GHIDRA_LANGUAGE_ID only allowed LE or BE as the endian field and no '/' in the processor, so it rejected four language IDs that ship with Ghidra: ARM:LEBE:32:v7LEInstruction and v8LEInstruction (ARM BE8) and M16C/60:LE:16:default and M16C/80:LE:16:default. The syntax check now allows LEBE and a slash between processor name parts; the installed .ldefs catalog check is unchanged.
2026-10-25 13:45:11 +08:00
morluto 8e6a957886 fix(release): exclude Copilot from human contribution credits
Preserve GitHub User metadata for Copilot while excluding it from generated human thanks and accepted prose credits. Extend the real inventory/render/check regression and document the known agent identity.
2026-10-25 12:28:21 +08:00
73 changed files with 46191 additions and 203 deletions

No files matched your search

+4 -3
View File
@@ -68,9 +68,10 @@ closing-issue reporters. Grouping preserves all those credits in one line.
When Highlights, detailed entries or migrations reference the same work,
put its thanks in one of those entries, not all of them. Include issue links.
Direct-commit bullets carry the commit link and known human credit.
Exclude GitHub bots and known agent accounts. REA's observed Anthropic, Cursor
and Codex co-author trailers resolve to `@claude`, `@cursoragent` and `@codex`
with GitHub type `User`; the credit policy excludes them while retaining their metadata.
Exclude GitHub bots and known agent accounts. REA's observed Anthropic, Cursor,
Codex, and Copilot co-author trailers resolve to `@claude`, `@cursoragent`,
`@codex`, and `@Copilot` with GitHub type `User`; the credit policy excludes
them while retaining their metadata.
At the REA maintainer's request, omit `@morluto` self-thanks while preserving
authorship in the inventory. Other human contributors remain eligible.
Use explicit GitHub links for cross-repository references to avoid ambiguous
+6
View File
@@ -55,6 +55,12 @@ jobs:
- uses: ./.github/actions/portable-build-cache
with:
build_cache_artifact: ${{ inputs.build_cache_artifact }}
- name: Install ELF32 cross toolchains for source-owned fixtures
run: |
sudo apt-get update
sudo apt-get install --yes --no-install-recommends \
gcc-i686-linux-gnu gcc-arm-linux-gnueabi gcc-mips-linux-gnu gcc-mipsel-linux-gnu \
libc6-dev-i386-cross libc6-dev-armel-cross libc6-dev-mips-cross libc6-dev-mipsel-cross
- name: Acquire exact upstream libraries in an isolated runner environment
shell: bash
run: |
+1 -1
View File
@@ -1,3 +1,3 @@
{
".": "6.7.0"
".": "6.8.0"
}
+102
View File
@@ -1,5 +1,107 @@
# Changelog
## [6.8.0](https://github.com/morluto/rea/compare/rea-agents-6.7.0...rea-agents-6.8.0) (2026-10-25)
### Highlights
- Build local agent environments from network captures, browser scenarios, or a prompt, then serve derived reads and evidence-gated writes and compare them with fresh application captures.
- Inspect more shipped formats, including GPU code objects, BEAM, Python bytecode, Unity IL2CPP metadata, Godot PCK, PyInstaller, Node.js SEA, devicetree, and additional PE/ELF tables.
- Keep large investigations focused with retained summaries, selected fact queries, configurable recovery limits, and row-oriented CLI exports. Complete Evidence remains available.
### ⚠ BREAKING CHANGES
- **Function analysis:** `analyze_function` now defaults to a retained summary. Set `detail: "complete"` for the previous inline dossier; the CLI retains its complete default and requires `--evidence_output` when selecting a summary. [#5660](https://github.com/morluto/rea/pull/5660). Thanks @CryptoJones, @gensokyo-dev, @RishTheFish21.
- **Accessibility:** Accept JSON numbers and booleans in node `value`, alongside strings and null. Update consumers that previously assumed text-only values. [cc90771](https://github.com/morluto/rea/commit/cc90771af14f045538651615147d8bf86b576549), [#6759](https://github.com/morluto/rea/pull/6759), [#6467](https://github.com/morluto/rea/issues/6467). Thanks @coygeek, @gensokyo-dev, @mvanhorn.
- **JSON Lines:** `--format jsonl` now emits object-collection rows followed by a `$rea: "jsonl/v1"`, `type: "metadata"` record; scalar selections and unsupported reports are refused. Parse control records separately, select a single supported collection when necessary, and use `--format json` for the previous whole-document output. See [JSON Lines framing](https://github.com/morluto/rea/blob/rea-agents-6.8.0/docs/cli.md#json-lines-record-output). [#5629](https://github.com/morluto/rea/pull/5629), [#5487](https://github.com/morluto/rea/issues/5487). Thanks @gensokyo-dev, @jayykernel, @vijaykiran06.
- **Interface Builder:** Keyed-archive graph identities use decimal table UIDs instead of authored `objectID`/`object-id`. Treat authored IDs as attributes and update persisted graph lookups; ibtool and native NIBArchive identities keep their source semantics. [6fbe67d](https://github.com/morluto/rea/commit/6fbe67d66e3d716db69929503a3a232a7ada0860).
- **Frida:** `instrument-with-frida` JSON returns the canonical Evidence directly. Read `normalized_result.cleanup_error` instead of the former top-level wrapper field. [48f700e](https://github.com/morluto/rea/commit/48f700ee35e0988ed973e5486dc9db33f2a5d944).
- **Reconstruction proof:** ID-array proof records no longer establish verified or ready ledgers. Supply implementation/revision bindings and individual claim outcomes; handle inapplicable, unavailable, and failed proof diagnostics. [f8b65e7](https://github.com/morluto/rea/commit/f8b65e78723b1e2169f94dd1226198ea3139707e).
- **Mach-O imports:** Handle a null import `name` when identity is ambiguous; use `raw_name` and `name_status` to distinguish observed expressions from exact symbols. [5d0d243](https://github.com/morluto/rea/commit/5d0d243a45c9132e1aff75d454ab53d86a74cc31), [169512a](https://github.com/morluto/rea/commit/169512a35bb015ff209168fb8bc1c9b31a2c678b).
- **Node.js:** Use a runtime satisfying `^22.23.4 || ^24.22.1 || >=26.12.1`; Node 23 and 25 are unsupported. Upgrade Node before installing or updating REA; the installer does not upgrade it. [#6315](https://github.com/morluto/rea/pull/6315), [#6283](https://github.com/morluto/rea/issues/6283). Thanks @GamerDubz, @gensokyo-dev, @ShikamaruNaaraa.
- **Browser storage:** Historical hashed storage remains readable, but comparisons stay unknown until both captures are recaptured in the new representation. [#6341](https://github.com/morluto/rea/pull/6341), [#6289](https://github.com/morluto/rea/issues/6289). Thanks @coygeek, @gongfpp.
- **Environment serving:** `environment-serve` serves derived state by default, including write tools whose effects are confirmed by recorded evidence. Use `--replay-only` for stateless replay of manifest tools. Read stop-summary `state` instead of `state_evidence_id`. [#5351](https://github.com/morluto/rea/pull/5351), [#6158](https://github.com/morluto/rea/pull/6158). Thanks @earakely-scale, @gensokyo-dev, @kaoru0822-kitauji.
- **MCP schemas:** Input advertisements default to compact. Set `REA_MCP_INPUT_SCHEMA_PROFILE=full` for complete advertised guidance and nested constraints. [#5975](https://github.com/morluto/rea/pull/5975). Thanks @gensokyo-dev.
- **Browser console arguments:** Accept null `argument_types` and `text_capture.status: "unavailable"` when the protocol argument inventory is omitted or malformed; preserve that uncertainty. [#6016](https://github.com/morluto/rea/pull/6016), [#5934](https://github.com/morluto/rea/issues/5934). Thanks @coygeek, @kaoru0822-kitauji.
- **Reconstruction summaries:** `summary.by_evidence_authority` is keyed by Evidence authority; read the former source-state counts from required `summary.by_source_state`. Regenerate ledgers whose closure digests depended on the old projection. [#5875](https://github.com/morluto/rea/pull/5875), [#5838](https://github.com/morluto/rea/issues/5838). Thanks @coygeek, @kaoru0822-kitauji.
- **Swift bindings:** Conformance `type_name` and `protocol_name` may be null; consume typed `type_binding`/`protocol_binding` evidence. Replace the former Objective-C-specific import-binding type/schema with `MachoImportBinding`. [d0067ba](https://github.com/morluto/rea/commit/d0067ba34a39ea47dd7e11bb2e6169bc0938cc3f).
- **WebSocket frames:** Accept null opcodes and preserve unknown frame facts; numeric opcodes are restricted to the four-bit protocol range. [0bc2b42](https://github.com/morluto/rea/commit/0bc2b42001f684beffcf47b70e507443b1a5d320).
- **Resource-only inspection:** Evidence subjects and executable digests may be null. Use `resource_root` and each resource's reported provenance instead of requiring an executable target. [6a6adf7](https://github.com/morluto/rea/commit/6a6adf703fa44100643947d20fbc670f86260b1b).
- **Browser timestamps:** Console and session-timeline timestamps may be null when the protocol supplies no finite/comparable time. Keep unknown time explicit in sorting and comparisons. [690fc51](https://github.com/morluto/rea/commit/690fc518540bae982038a74f47ef7781201785aa), [2a0410f](https://github.com/morluto/rea/commit/2a0410f1717b3126e8a95063d77dea66a597010a).
- **Reference sources:** Read failed root enumeration from `rootFailure` rather than a synthetic failed child at `"."`; preserve the top-level source-graph limitation. [6296fdf](https://github.com/morluto/rea/commit/6296fdf560f1ac4aa55985558edaa98300aacdbc).
- **Filesystem capture:** Accept `entry_unavailable` and `identity_changed` enumeration omission reasons in addition to the previous reason literals. [c85b44e](https://github.com/morluto/rea/commit/c85b44e1b14851c54e65327a4807f79ad5cd5a61). Thanks @rudrakshagarwal4200-ai.
- **APK locales:** Replace numeric legacy selectors such as `es-r419` with `es-419` or `b+es+419`. Two-letter regions remain supported. [29910d1](https://github.com/morluto/rea/commit/29910d16b98fe4b607a7aa15928789a73614b0b9). Thanks @mertkayacs.
- **XML plist scalars:** Recognize `$plist_type: "unknown"` and read `element`, `literal`, and `reason`; unresolved XML scalars use this wrapper instead of the former real/null unknown marker or malformed scalar prefixes. Supported extended UTC years preserve their timestamp. [3537e58](https://github.com/morluto/rea/commit/3537e58c1ab29309f06eabf50bc52adba9817a0c).
### Changes
- **Binary layouts:** Inspect ELF32 i386, little-endian ARM, and little- or big-endian MIPS `EXEC`/`DYN`/`REL` layouts on Linux x64 using a caller-supplied pwntools environment. [#7052](https://github.com/morluto/rea/pull/7052), [#6934](https://github.com/morluto/rea/issues/6934). Thanks @roblennon, @salmanalmutairi1.
- **Agent environments:** Project network captures and Evidence files into manifests, preserve template path pairs and positions, export AgentEnv bundles, and serve manifests as replay environments. [#4954](https://github.com/morluto/rea/pull/4954), [#5293](https://github.com/morluto/rea/pull/5293), [#5664](https://github.com/morluto/rea/pull/5664), [#4999](https://github.com/morluto/rea/pull/4999), [#4975](https://github.com/morluto/rea/pull/4975). Thanks @earakely-scale, @gensokyo-dev, @kaoru0822-kitauji.
- **Capture to environment:** Build environments from browser scenarios or a prompt, extract the initial observed state, and compare the environment with application probe captures. [#5182](https://github.com/morluto/rea/pull/5182), [#6946](https://github.com/morluto/rea/pull/6946), [#5319](https://github.com/morluto/rea/pull/5319), [#5699](https://github.com/morluto/rea/pull/5699). Thanks @earakely-scale, @gensokyo-dev, @kaoru0822-kitauji.
- **State-backed tools:** Render read filters, search, sorting and paging from state; apply confirmed write effects with broader state targets and recording-based write admission. [#5352](https://github.com/morluto/rea/pull/5352), [293832b](https://github.com/morluto/rea/commit/293832bafc9a054914ec514fe906dd4936c7a372), [#5800](https://github.com/morluto/rea/pull/5800), [#5787](https://github.com/morluto/rea/pull/5787), [#5600](https://github.com/morluto/rea/pull/5600), [5375ff0](https://github.com/morluto/rea/commit/5375ff0f9aadb4fd0d46333815aa0431e3df1010), [#6169](https://github.com/morluto/rea/pull/6169), [#6385](https://github.com/morluto/rea/pull/6385), [#6897](https://github.com/morluto/rea/pull/6897). Thanks @earakely-scale, @gensokyo-dev, @kaoru0822-kitauji.
- **State learning:** Model application-assigned values, preserve view-specific clock spellings, learn write clocks from scenario timing, derive fields, and learn creates across sorted arrays and referenced entities. [7ec9b6e](https://github.com/morluto/rea/commit/7ec9b6e56e1b3762285dca881db6d900a389ab9c), [#6136](https://github.com/morluto/rea/pull/6136), [957bcfa](https://github.com/morluto/rea/commit/957bcfa149c1a0593bb0aa2466cc56d1d4f62e7d), [#6138](https://github.com/morluto/rea/pull/6138), [#6967](https://github.com/morluto/rea/pull/6967), [#7025](https://github.com/morluto/rea/pull/7025), [#7034](https://github.com/morluto/rea/pull/7034). Thanks @earakely-scale.
- **Recorded writes:** Serve creates whose compatible read-backs add omitted fields or express timestamps at a different precision, and whole-object updates that regenerate observed slugs or modification times. Conflicting observations still withhold writes. State views and bundles carrying the new read-back data need REA 6.8.0 or later to serve. [#7080](https://github.com/morluto/rea/pull/7080). Thanks @earakely-scale.
- **Native investigations:** Page native inventory summaries, match functions across builds, export pseudocode to resumable directories, and select objdump disassembly by symbol. [#5378](https://github.com/morluto/rea/pull/5378), [#5390](https://github.com/morluto/rea/pull/5390), [#6170](https://github.com/morluto/rea/pull/6170), [#5171](https://github.com/morluto/rea/pull/5171), [#4249](https://github.com/morluto/rea/issues/4249), [#5343](https://github.com/morluto/rea/issues/5343), [#5159](https://github.com/morluto/rea/issues/5159). Thanks @coygeek, @CryptoJones, @gensokyo-dev, @kaoru0822-kitauji.
- **Engine workflows:** Bound a single Ghidra decompilation without closing its session, analyze GameCube/Wii DOL executables, and inspect static CUDA/HIP code objects. [#5662](https://github.com/morluto/rea/pull/5662), [#6240](https://github.com/morluto/rea/pull/6240), [#5813](https://github.com/morluto/rea/pull/5813), [#5340](https://github.com/morluto/rea/issues/5340), [#5345](https://github.com/morluto/rea/issues/5345). Thanks @arielfikru, @coygeek, @gensokyo-dev, @RishTheFish21.
- **PE/ELF:** Inspect export tables without an engine, PE imports/delay-loads, Authenticode certificates, TLS callbacks/load configuration, ELF notes, Linux kernel module metadata, and Zstandard section-name tables. [#6278](https://github.com/morluto/rea/pull/6278), [#6509](https://github.com/morluto/rea/pull/6509), [#4830](https://github.com/morluto/rea/pull/4830), [#7008](https://github.com/morluto/rea/pull/7008), [#4837](https://github.com/morluto/rea/pull/4837), [#7010](https://github.com/morluto/rea/pull/7010), [#6987](https://github.com/morluto/rea/pull/6987), [#4057](https://github.com/morluto/rea/issues/4057). Thanks @gensokyo-dev, @kaoru0822-kitauji, @mvanhorn, @Rinkia, @ShuaixinHuang.
- **Apple/native metadata:** Inspect IPS crash reports, decode Swift nominal declarations, and select universal Mach-O slices in Go/Rust metadata readers. [#6511](https://github.com/morluto/rea/pull/6511), [#6257](https://github.com/morluto/rea/pull/6257), [#6219](https://github.com/morluto/rea/pull/6219), [#5341](https://github.com/morluto/rea/issues/5341), [#4102](https://github.com/morluto/rea/issues/4102). Thanks @coygeek, @gensokyo-dev, @kaoru0822-kitauji, @mvanhorn, @Rinkia.
- **Packaged applications:** Triage desktop installation directories and inspect/extract Godot PCK, PyInstaller archives, and Node.js single-executable application blobs. [#5207](https://github.com/morluto/rea/pull/5207), [#3634](https://github.com/morluto/rea/pull/3634), [afa5da7](https://github.com/morluto/rea/commit/afa5da745e0b139e68029a96b776ae30e48ea2b2), [#2433](https://github.com/morluto/rea/pull/2433), [#7000](https://github.com/morluto/rea/pull/7000), [#4399](https://github.com/morluto/rea/issues/4399). Thanks @0xthearchitect, @coygeek, @gensokyo-dev, @mvanhorn.
- **Unity and managed code:** Inventory IL2CPP images/types/methods and metadata versions 24/27; read member names and literals, link native methods and generic instances, and export Ghidra seeds. Project managed call/field-access edges and page managed member summaries. [#3807](https://github.com/morluto/rea/pull/3807), [#5326](https://github.com/morluto/rea/pull/5326), [52d1a72](https://github.com/morluto/rea/commit/52d1a72af047a73b787b8935b032786a34966036), [#5327](https://github.com/morluto/rea/pull/5327), [#5785](https://github.com/morluto/rea/pull/5785), [#6260](https://github.com/morluto/rea/pull/6260), [#5167](https://github.com/morluto/rea/pull/5167), [#3784](https://github.com/morluto/rea/issues/3784), [#5112](https://github.com/morluto/rea/issues/5112), [#4056](https://github.com/morluto/rea/issues/4056), [#5148](https://github.com/morluto/rea/issues/5148). Thanks @8rulerstar, @alexlanghart, @elricfd, @gensokyo-dev, @jaelliot, @kaoru0822-kitauji, @Rinkia.
- **Android:** Inspect APK signing declarations and structured manifest components; report static-final DEX field values. [e93e3b1](https://github.com/morluto/rea/commit/e93e3b1e7d6e7e4f49c2de24b92452ec29e853f8), [#4158](https://github.com/morluto/rea/pull/4158), [#5025](https://github.com/morluto/rea/pull/5025), [#6981](https://github.com/morluto/rea/pull/6981), [#4537](https://github.com/morluto/rea/issues/4537), [#6397](https://github.com/morluto/rea/issues/6397). Thanks @Abelo9996, @BaaRai, @FerroLx, @gensokyo-dev, @sserrgo12.
- **Bytecode and firmware:** Inspect and query CPython pyc and BEAM facts, inspect flattened devicetrees, and attribute MMIO peripheral regions in native disassembly. [#4826](https://github.com/morluto/rea/pull/4826), [#6928](https://github.com/morluto/rea/pull/6928), [#4833](https://github.com/morluto/rea/pull/4833), [#6943](https://github.com/morluto/rea/pull/6943), [#7009](https://github.com/morluto/rea/pull/7009), [#5348](https://github.com/morluto/rea/pull/5348), [#6922](https://github.com/morluto/rea/issues/6922), [#6938](https://github.com/morluto/rea/issues/6938). Thanks @gensokyo-dev, @liziye627-design, @mvanhorn.
- **JavaScript:** Retain queryable static route inventories, link assignment values to writes, and accept source selections as a JSON array or file. [#6019](https://github.com/morluto/rea/pull/6019), [#6206](https://github.com/morluto/rea/pull/6206), [#5029](https://github.com/morluto/rea/pull/5029), [#4772](https://github.com/morluto/rea/issues/4772), [#6193](https://github.com/morluto/rea/issues/6193). Thanks @gensokyo-dev, @IvenKooLab, @jacobmalibiran, @JoshOsullivan-au, @kaoru0822-kitauji, @KJLavender.
- **Data inspection:** Decode selected protobuf/gRPC bodies, inspect GGUF/safetensors declarations, and produce byte-entropy profiles. [#5031](https://github.com/morluto/rea/pull/5031), [#5603](https://github.com/morluto/rea/pull/5603), [#2331](https://github.com/morluto/rea/pull/2331), [#4986](https://github.com/morluto/rea/issues/4986), [#3323](https://github.com/morluto/rea/issues/3323). Thanks @AayushJB03, @gensokyo-dev, @kaoru0822-kitauji, @KJLavender, @michaelkillgta, @mvanhorn.
- **CLI input/output:** Accept structured request documents on stdin and expose version/runtime output formats. Row-oriented JSONL exports use the migration above. [#7040](https://github.com/morluto/rea/pull/7040), [#5486](https://github.com/morluto/rea/issues/5486), [#6530](https://github.com/morluto/rea/pull/6530), [169512a](https://github.com/morluto/rea/commit/169512a35bb015ff209168fb8bc1c9b31a2c678b), [#5483](https://github.com/morluto/rea/issues/5483). Thanks @Abelo9996, @ahmed-husssain, @CassieFox23, @DeryFerd, @eco-null, @ezrabrilliant, @GamerDubz, @gensokyo-dev, @gongfpp, @hifzabuildsai, @jayykernel, @mhamzasakhi, @pablodiazjorge, @Plastic-time, @raidnouredden-ux, @SandstromPL, @ShuaixinHuang, @Sriram-PR, @XiaoBinGan.
- **MCP and setup:** Add opt-in Streamable HTTP and FreeBuff setup registration. FreeBuff support first appears in 6.8.0; installed native FreeBuff loading and skill discovery remain unverified. [#4955](https://github.com/morluto/rea/pull/4955), [#2736](https://github.com/morluto/rea/pull/2736), [#3243](https://github.com/morluto/rea/issues/3243). Thanks @0xLLVEL, @codebuff-team, @earakely-scale, @gensokyo-dev, @kaoru0822-kitauji.
- **Recovery controls:** Adjust per-call WASM/SQLite/firmware deadlines, MCP startup budgets, regex matching deadlines/worker heap, capture decoder heap, ASAR header admission, Apple dispatch input, and the bridge request ceiling. MCP runtime limit recovery reports actionable settings. [#5798](https://github.com/morluto/rea/pull/5798), [#5818](https://github.com/morluto/rea/pull/5818), [aa53c06](https://github.com/morluto/rea/commit/aa53c065387c58b4189d82c47f1f3445fc505e30), [#5863](https://github.com/morluto/rea/pull/5863), [#6304](https://github.com/morluto/rea/pull/6304), [#6267](https://github.com/morluto/rea/pull/6267), [#6301](https://github.com/morluto/rea/pull/6301), [#6299](https://github.com/morluto/rea/pull/6299), [#6209](https://github.com/morluto/rea/pull/6209), [#6233](https://github.com/morluto/rea/pull/6233), [#6376](https://github.com/morluto/rea/pull/6376), [#7049](https://github.com/morluto/rea/pull/7049), [#5797](https://github.com/morluto/rea/issues/5797), [#5812](https://github.com/morluto/rea/issues/5812), [#5851](https://github.com/morluto/rea/issues/5851), [#6176](https://github.com/morluto/rea/issues/6176), [#3917](https://github.com/morluto/rea/issues/3917). Thanks @gensokyo-dev, @jaychou813, @kaoru0822-kitauji.
- **APK output:** Set `string_tables_only` or CLI `--string-tables-only` to omit duplicate primary strings and avoid their output-capacity charge. [7c458f0](https://github.com/morluto/rea/commit/7c458f04b9155707c8ba406f8b455b10016f1b99), [#4990](https://github.com/morluto/rea/pull/4990), [#4982](https://github.com/morluto/rea/issues/4982). Thanks @gensokyo-dev, @kaoru0822-kitauji, @N0zoM1z0.
- **Retained analysis:** Reuse successful validation of authenticated immutable native/imported-JavaScript parents and captures. Query retained Go build identity, Rust dependencies, JavaScript source-to-bundle mappings, and native export facts without repeating full output. [8f7d12e](https://github.com/morluto/rea/commit/8f7d12e30d63f3d3b4cfe2a90336362841438f56), [#4962](https://github.com/morluto/rea/pull/4962), [#5349](https://github.com/morluto/rea/pull/5349), [#5524](https://github.com/morluto/rea/pull/5524), [#5815](https://github.com/morluto/rea/pull/5815), [#6128](https://github.com/morluto/rea/pull/6128), [#6907](https://github.com/morluto/rea/pull/6907), [#4810](https://github.com/morluto/rea/issues/4810), [#5499](https://github.com/morluto/rea/issues/5499), [#5802](https://github.com/morluto/rea/issues/5802), [#6112](https://github.com/morluto/rea/issues/6112), [#6900](https://github.com/morluto/rea/issues/6900). Thanks @gensokyo-dev, @kaoru0822-kitauji, @N0zoM1z0.
- **Compact results:** Use retained Frida/GDB/objdump summaries, omit redundant Ghidra raw results, and defer the analysis runtime during MCP catalog discovery. [#5661](https://github.com/morluto/rea/pull/5661), [#5614](https://github.com/morluto/rea/pull/5614), [#5355](https://github.com/morluto/rea/pull/5355), [#4424](https://github.com/morluto/rea/pull/4424), [#5640](https://github.com/morluto/rea/issues/5640), [#5584](https://github.com/morluto/rea/issues/5584), [#5337](https://github.com/morluto/rea/issues/5337). Thanks @coygeek, @gensokyo-dev, @kaoru0822-kitauji, @leeguooooo.
- **Analysis work:** Reuse Hopper literal-string records, JavaScript workers and captured prefix selections, Interface Builder validation and plist scalar schemas; bound stable reads to one snapshot buffer and avoid decoding omitted GGUF strings. [#5017](https://github.com/morluto/rea/pull/5017), [#5819](https://github.com/morluto/rea/pull/5819), [31de177](https://github.com/morluto/rea/commit/31de177404461d122761d8243abe4fd24564f4da), [#5885](https://github.com/morluto/rea/pull/5885), [85e38ac](https://github.com/morluto/rea/commit/85e38ac199dff15e8c43343d9170db7e0e46d284), [#6966](https://github.com/morluto/rea/pull/6966), [#5781](https://github.com/morluto/rea/pull/5781), [#5010](https://github.com/morluto/rea/issues/5010), [#5855](https://github.com/morluto/rea/issues/5855), [#6954](https://github.com/morluto/rea/issues/6954), [#5774](https://github.com/morluto/rea/issues/5774). Thanks @gensokyo-dev, @kaoru0822-kitauji.
- **Native scheduling:** Batch modern IDA callee lookups, schedule decompilation reads on demand, index NativeAOT type closure, and retain call-path ancestry once per visited node. [0c6fed9](https://github.com/morluto/rea/commit/0c6fed90b74c75a9fd23d1ec1b510a7274f85887), [06fe7ee](https://github.com/morluto/rea/commit/06fe7eecbc8c387414b4c1aa0d88ad6314f54f0c), [ce61680](https://github.com/morluto/rea/commit/ce61680c04c95a9cdfad5335632a99542af26d37), [5e4ec0f](https://github.com/morluto/rea/commit/5e4ec0fa1b678aa3bf20704ec9110224ba8c1676).
- **Selected processing:** Prepare CLI positional schemas only for the selected command, separate entropy reads from report resolution, prune typed-array reference escapes, index state-write cutoffs, decode only selected replay query values, and skip payload text during WAT framing. [315b234](https://github.com/morluto/rea/commit/315b23403f0819967878cee418c3f9cb4b1a69d9), [#6950](https://github.com/morluto/rea/pull/6950), [#5139](https://github.com/morluto/rea/pull/5139), [#5350](https://github.com/morluto/rea/pull/5350), [96ea32b](https://github.com/morluto/rea/commit/96ea32b7e4e28d3c9cd79a1de4b8e1ba3b4244e9), [70835d3](https://github.com/morluto/rea/commit/70835d3d5f030deadf606e1c79c70a60883e50c9), [#5766](https://github.com/morluto/rea/pull/5766), [#6947](https://github.com/morluto/rea/issues/6947), [#5760](https://github.com/morluto/rea/issues/5760). Thanks @galleonli, @gensokyo-dev, @kaoru0822-kitauji.
### Fixes
- **IPS report fields:** Preserve the recorded terminating process and PID, termination flags, parent PID, and build/platform metadata. [#7079](https://github.com/morluto/rea/pull/7079), [#7078](https://github.com/morluto/rea/issues/7078). Thanks @rodricksz4h5.
- **Ghidra language selection:** Accept stock language IDs containing `LEBE` or slash-separated processor names, and report conflicting DOL language overrides as configuration errors naming `REA_GHIDRA_LANGUAGE_ID`. [#7070](https://github.com/morluto/rea/pull/7070), [#7072](https://github.com/morluto/rea/issues/7072), [#7081](https://github.com/morluto/rea/pull/7081), [#7082](https://github.com/morluto/rea/issues/7082). Thanks @Adolanium.
- **Setup and Hopper:** Preserve the Linux embedding runtime, supported x86_64 host restriction, readiness-based provider choices, orderly demo-helper shutdown, and installed apt dependency versions. Keep cleanup and skill publication ownership through retries. [#5041](https://github.com/morluto/rea/pull/5041), [#3422](https://github.com/morluto/rea/pull/3422), [f5d1012](https://github.com/morluto/rea/commit/f5d1012fabfe99af82523c3078ce9d93eb3780c1), [#4966](https://github.com/morluto/rea/pull/4966), [#6974](https://github.com/morluto/rea/pull/6974), [211e353](https://github.com/morluto/rea/commit/211e3533b34f81ae0910ad4f53f2a3d3c8587d3e), [#6920](https://github.com/morluto/rea/pull/6920), [#5015](https://github.com/morluto/rea/issues/5015), [#3419](https://github.com/morluto/rea/issues/3419), [#4921](https://github.com/morluto/rea/issues/4921), [#6975](https://github.com/morluto/rea/issues/6975). Thanks @Abel-I-Akhigbe, @Adolanium, @DavidFB-creator, @DeryFerd, @gensokyo-dev, @kaoru0822-kitauji, @sx4im.
- **Client setup:** Treat empty APPDATA as unset, distinguish MSIX Claude Desktop process views, preserve aliased Hermes server tables, reuse client dialect/alias bindings, and materialize the pinned package-runner cache before the first start. [#5126](https://github.com/morluto/rea/pull/5126), [#4903](https://github.com/morluto/rea/pull/4903), [#3785](https://github.com/morluto/rea/pull/3785), [b7eeded](https://github.com/morluto/rea/commit/b7eededf5af5061a834f93f57f2c888acf870344), [#7032](https://github.com/morluto/rea/pull/7032), [#5104](https://github.com/morluto/rea/issues/5104), [#4278](https://github.com/morluto/rea/issues/4278), [#3320](https://github.com/morluto/rea/issues/3320), [#7027](https://github.com/morluto/rea/issues/7027). Thanks @CmsChase, @coygeek, @myso-kr, @pablodiazjorge, @vaidehi-snk, @wxj006007.
- **Target admission:** Preserve thin arm64_32 Mach-O support, distinguish JVM identity before FAT admission, validate FAT format/CPU identity, preserve original DOS header padding, and reject NULs in local objdump/rizin paths. [#5788](https://github.com/morluto/rea/pull/5788), [6843dbe](https://github.com/morluto/rea/commit/6843dbea9bce4709ee2e8bf46c38f0bbf640716b), [497037e](https://github.com/morluto/rea/commit/497037e6e3975cbb37cfd89ea0ad300faa2a4769), [#7013](https://github.com/morluto/rea/pull/7013), [#3600](https://github.com/morluto/rea/pull/3600), [#5713](https://github.com/morluto/rea/issues/5713), [#6510](https://github.com/morluto/rea/issues/6510). Thanks @coygeek, @dhruvac29, @kaoru0822-kitauji, @Lew1sWong, @qorvr.
- **Mach-O and dylibs:** Preserve container framing, facts when dyld rejects a missing UUID, verified-signature snapshot identity, unsigned diagnostics, absolute paths inside the selected root, legacy platform rules, lazy-load payloads, and reasons for rejecting present dylibs. [cf3288d](https://github.com/morluto/rea/commit/cf3288d8cf7a09685acf89103b36c32359be56fa), [2b263a7](https://github.com/morluto/rea/commit/2b263a7fe9e9d4c185c7d6af6b4e67b21a73af30), [eb7acc7](https://github.com/morluto/rea/commit/eb7acc70bf06f119e8639e30a78081d5ba82b0bf), [1502cfb](https://github.com/morluto/rea/commit/1502cfbc8ca0d1d1d86eebf334018dfb418bbac0), [90062c0](https://github.com/morluto/rea/commit/90062c092228dea9d33fadc0d7e1b968105b8b2c), [#5650](https://github.com/morluto/rea/pull/5650), [3b67b36](https://github.com/morluto/rea/commit/3b67b362aa2cd79eac5af57d0965e81a7d0b8c2b), [#5551](https://github.com/morluto/rea/issues/5551). Thanks @coygeek.
- **Native facts:** Admit direct dispatch-metadata providers; preserve record-local Objective-C omissions, snapshot profiles, class-import binding identity, stripped class labels, version-like names, and qualified Swift protocol identities. [50b6975](https://github.com/morluto/rea/commit/50b6975d861069e2c6cfa202780621449f863802), [9eec57b](https://github.com/morluto/rea/commit/9eec57b469087fe9bff3d29dcc02f47074159e91), [0ba4d6f](https://github.com/morluto/rea/commit/0ba4d6f43ad610dacc861a1548be1a5c4712cd61), [4d7de00](https://github.com/morluto/rea/commit/4d7de00d4cc9a95ec2b7a3123b04e01db2daafa0), [22c84ce](https://github.com/morluto/rea/commit/22c84ce64aa6c7601aa81af86d4117b5a4c2feac), [4852c83](https://github.com/morluto/rea/commit/4852c830af1e3b9df437235b7a8fa8970a6d1c59), [aa9043f](https://github.com/morluto/rea/commit/aa9043f6b61c453e4735795cfe6a559bf6b6cc16).
- **Apple resources:** Preserve independent plist scalars, processing instructions, nonstandard table widths, unsupported NIB connector coverage, and strict IPS JSON surrogate validation; classify Interface Builder objects by their declared superclass chains. [fd728f2](https://github.com/morluto/rea/commit/fd728f271e7de7bcfa63890b573715720d5f948c), [#5806](https://github.com/morluto/rea/pull/5806), [4526862](https://github.com/morluto/rea/commit/4526862728af01f1135714079c0d8a8c754eb364), [14cd78c](https://github.com/morluto/rea/commit/14cd78c3224edcf79fca152915077112f547b9ed), [#6990](https://github.com/morluto/rea/pull/6990), [#5805](https://github.com/morluto/rea/issues/5805), [#5228](https://github.com/morluto/rea/pull/5228), [#5218](https://github.com/morluto/rea/issues/5218). Thanks @coygeek, @DeryFerd, @kaoru0822-kitauji, @Mangolycheematcha, @mhamzasakhi, @Shreyash021104.
- **Archives and files:** Complete positional ZIP reads, preserve bounded ASAR signatures, report stable symlink loops as resolution failures, retain target applicability, and report unknown traversal coverage. Retain reported integrity contradictions for members present in only one compared artifact. [#4414](https://github.com/morluto/rea/pull/4414), [e8da7e1](https://github.com/morluto/rea/commit/e8da7e1edd075feec0e6beedec93d5645fed8125), [#5571](https://github.com/morluto/rea/pull/5571), [#5763](https://github.com/morluto/rea/pull/5763), [#5867](https://github.com/morluto/rea/pull/5867), [#5521](https://github.com/morluto/rea/issues/5521), [#5731](https://github.com/morluto/rea/issues/5731), [#5837](https://github.com/morluto/rea/issues/5837), [#7076](https://github.com/morluto/rea/pull/7076). Thanks @Afloat16, @coygeek, @jdotc1, @kaoru0822-kitauji, @WenyuChiou.
- **Extraction and file ownership:** Restrict destination-case recovery to owned collisions, retain physical-read failures through late cancellation/cleanup, preserve cleanup capabilities for retry, and retain independent firmware extraction failures. [932fbb7](https://github.com/morluto/rea/commit/932fbb7d84a9a995b9aee3c1be7da05a8c4d7bbb), [9499667](https://github.com/morluto/rea/commit/949966712e68d8857a50a165ecddf04c2d7e6d3b), [56679dd](https://github.com/morluto/rea/commit/56679dd2ae31dda58e181f8bf99775014f7bb82b), [b3c912a](https://github.com/morluto/rea/commit/b3c912ab79ad147a37b6cfbe1552a1a5eecec629), [7298bd0](https://github.com/morluto/rea/commit/7298bd08d134860b84b4073f6703021916a5a4c5).
- **Processes and cancellation:** Retain partial samples and shutdown gaps, unresolved descendants and caller abort reasons; bind cleanup errors to their owners, propagate comparison contradictions, and report no normal exit code for signalled targets. [ba866a2](https://github.com/morluto/rea/commit/ba866a26ecbe179a717e746cbc0d1605ae616ad5), [#5270](https://github.com/morluto/rea/pull/5270), [#6144](https://github.com/morluto/rea/pull/6144), [a721c42](https://github.com/morluto/rea/commit/a721c42c07dc674c32ab939627355b8a1bee97a5), [#6298](https://github.com/morluto/rea/pull/6298), [#6486](https://github.com/morluto/rea/pull/6486), [#5935](https://github.com/morluto/rea/issues/5935), [#5944](https://github.com/morluto/rea/issues/5944), [#6100](https://github.com/morluto/rea/issues/6100), [#6101](https://github.com/morluto/rea/issues/6101), [#6110](https://github.com/morluto/rea/issues/6110), [#6113](https://github.com/morluto/rea/issues/6113), [#6121](https://github.com/morluto/rea/issues/6121), [#6131](https://github.com/morluto/rea/issues/6131), [#6132](https://github.com/morluto/rea/issues/6132), [#6218](https://github.com/morluto/rea/issues/6218), [#6445](https://github.com/morluto/rea/issues/6445). Thanks @agorevski, @coygeek, @gongfpp, @kwakhyun, @mhamzasakhi.
- **Provider cleanup:** Preserve Windows native handles until close is confirmed, native/Inspector/Electron cleanup ownership, Kuna probe identity and cleanup, IDA registration configuration, and Hopper preparation ownership. [#5772](https://github.com/morluto/rea/pull/5772), [87ab054](https://github.com/morluto/rea/commit/87ab054ea384b3fcbae2998b1f94685edcd5f624), [ff78165](https://github.com/morluto/rea/commit/ff78165978de9dbcf9fa16c2acf74bf85a69dc44), [04ae07a](https://github.com/morluto/rea/commit/04ae07ab8c9ae1a6697b6ad15489bbde515da8b5), [dd0015a](https://github.com/morluto/rea/commit/dd0015aaf156c5701ef9598bcc6c885e6b8da803), [b231c28](https://github.com/morluto/rea/commit/b231c28cf64c2f799ada99343f8f21e0e6acb15e), [#4505](https://github.com/morluto/rea/issues/4505), [#5616](https://github.com/morluto/rea/issues/5616).
- **Provider diagnostics:** Give Ghidra restart recovery, stable Homebrew aliases, unsupported-host results for Linux-only integrations, correct IDA regex/readiness handling, and clear diagnostics when Windows refuses a verified native addon. [#5356](https://github.com/morluto/rea/pull/5356), [7c6142e](https://github.com/morluto/rea/commit/7c6142efdc19a42c148d0e8c96b69d18e63c7f87), [#5793](https://github.com/morluto/rea/pull/5793), [#4862](https://github.com/morluto/rea/pull/4862), [99ccfb6](https://github.com/morluto/rea/commit/99ccfb6e709ac4bc0afe176315280abde9ebcb9f), [#6832](https://github.com/morluto/rea/pull/6832), [#5730](https://github.com/morluto/rea/issues/5730), [#4860](https://github.com/morluto/rea/issues/4860), [#5723](https://github.com/morluto/rea/issues/5723). Thanks @coygeek, @dark13b, @gensokyo-dev, @jgt87, @jpmorris41488, @Lew1sWong.
- **MCP results:** Preserve protocol envelopes on oversized errors, enforce and advertise absolute Evidence paths, retain typed output-filter errors and selected JSON paths, and give client/provider timeout recovery. Compact advertisements now normalize reported enum and union shapes for strict gateway translation. [562dab0](https://github.com/morluto/rea/commit/562dab00c5cbf76a9b2695bad93792eccdc9a49d), [3cf6206](https://github.com/morluto/rea/commit/3cf6206614e65b5ebb6284a5709090dfb8f9aaee), [0e83d92](https://github.com/morluto/rea/commit/0e83d92c583ae36afe00b2c23dd2a0f1d025cbca), [4714c7e](https://github.com/morluto/rea/commit/4714c7eaa3d1d8b6a5df0aff586e844b9d9ab06d), [1bfa60a](https://github.com/morluto/rea/commit/1bfa60ac20e1a344bcf3132c08d15b6860b05c61), [#5608](https://github.com/morluto/rea/pull/5608), [#3806](https://github.com/morluto/rea/pull/3806), [#5598](https://github.com/morluto/rea/issues/5598), [#3800](https://github.com/morluto/rea/issues/3800). Thanks @gensokyo-dev, @Hahaa909, @wang1xiang.
- **Browser lifecycles:** Retain historical outcomes through cleanup, stop late navigation before session release, stop expired work, observe aborted fetch settlement, and mark later scenario actions skipped after failure or cancellation. [2e5171e](https://github.com/morluto/rea/commit/2e5171e391b83f8a467d0861fd452c1f78621fd8), [599fdac](https://github.com/morluto/rea/commit/599fdac029912f96c511f859660483405d2de26f), [b2fad36](https://github.com/morluto/rea/commit/b2fad362f2b37f81dc9a6f784f4521e4ca24d41e), [13a8640](https://github.com/morluto/rea/commit/13a864020414577006dce58d7fdaa877ddd0cb52), [#6023](https://github.com/morluto/rea/pull/6023), [#5943](https://github.com/morluto/rea/issues/5943). Thanks @coygeek, @kaoru0822-kitauji.
- **Browser comparisons:** Preserve frame identity, default origin scope, unknown body/shape coverage, correspondence and uncertainty, and independent retained-observation budgets. [#4040](https://github.com/morluto/rea/pull/4040), [#4413](https://github.com/morluto/rea/pull/4413), [#4476](https://github.com/morluto/rea/pull/4476), [a0c22ff](https://github.com/morluto/rea/commit/a0c22ff0125b8af7612db54d3708413e2bfecb25), [a0fcc60](https://github.com/morluto/rea/commit/a0fcc60d89eb0cc09b08181eb9f961acfd07d8e9), [#3878](https://github.com/morluto/rea/issues/3878), [#3910](https://github.com/morluto/rea/issues/3910), [#3941](https://github.com/morluto/rea/issues/3941), [#4385](https://github.com/morluto/rea/issues/4385), [#4477](https://github.com/morluto/rea/issues/4477). Thanks @Adolanium, @coygeek, @eco-null, @gensokyo-dev, @kaoru0822-kitauji, @XY-code1.
- **Electron observations:** Preserve listener callback ownership and count registrations by source callsite; block trash calls in the active hook, retain window handles through cancelled selection, and preserve cancellation during finalization. [dfe8378](https://github.com/morluto/rea/commit/dfe8378cf5a727e3d93d47a5986233175c6ef898), [5064007](https://github.com/morluto/rea/commit/506400748eb1d6c674f0cf51330073929c45f939), [816a9dc](https://github.com/morluto/rea/commit/816a9dc98886e6d824cd8ae88b68731b161d5dda), [1707d17](https://github.com/morluto/rea/commit/1707d171d750955bf6387e028e378183b360ffe3), [4e2a566](https://github.com/morluto/rea/commit/4e2a566d6c7e053ecbbee1b684311ed31ea163bf).
- **JavaScript source maps:** Keep unmapped and missing mappings unknown, preserve partial indexed-map facts and section paths, propagate preparation capability errors, and retain observed worker failures through cancellation. [a4dbb36](https://github.com/morluto/rea/commit/a4dbb36d8ea83e9f94dd289ac1a2d220d2c72c9b), [2b2e362](https://github.com/morluto/rea/commit/2b2e362a00fc5ba2f93fa6c62e2d8780072d1b4b), [498a667](https://github.com/morluto/rea/commit/498a667234b0140c1cfa43c819b35454ab11d14e), [#6149](https://github.com/morluto/rea/pull/6149), [#6214](https://github.com/morluto/rea/pull/6214), [f01dcca](https://github.com/morluto/rea/commit/f01dccab77f712fb4ac936d0a6e92a70d12f0789), [#6082](https://github.com/morluto/rea/issues/6082), [#6061](https://github.com/morluto/rea/issues/6061). Thanks @coygeek, @diyasharma05, @kaoru0822-kitauji.
- **JavaScript semantics:** Preserve overflowing timer uncertainty, unavailable-scope comparisons, directory-form URL/ESM path intent, and artifact confinement. Replaced globals lose built-in effects; erased imports no longer become runtime bindings. Treat a known truthy `fork()` `silent` option as piped stdio unless a string/array `stdio` override takes precedence; unresolved options stay dynamic. [#3456](https://github.com/morluto/rea/pull/3456), [8d198d6](https://github.com/morluto/rea/commit/8d198d6ec707402d75276f516e0090d71767b7e4), [#4646](https://github.com/morluto/rea/pull/4646), [#2700](https://github.com/morluto/rea/pull/2700), [#4565](https://github.com/morluto/rea/pull/4565), [#6994](https://github.com/morluto/rea/pull/6994), [#3400](https://github.com/morluto/rea/issues/3400), [#3882](https://github.com/morluto/rea/issues/3882), [#3367](https://github.com/morluto/rea/issues/3367), [#7083](https://github.com/morluto/rea/pull/7083), [#7084](https://github.com/morluto/rea/issues/7084). Thanks @Adolanium, @amyanger, @coygeek, @gensokyo-dev, @HawkForge, @rohitg00, @viniciusdsandrade.
- **JavaScript routes and imports:** Treat path data as route candidates, preserve route flag and lexical RPC identities, package-main trailing-slash selection, catch aliases, and factory parameter ownership. [#6360](https://github.com/morluto/rea/pull/6360), [38ecc02](https://github.com/morluto/rea/commit/38ecc0248e1993d25a2f7dd350024484cce797bb), [#4219](https://github.com/morluto/rea/pull/4219), [21097b1](https://github.com/morluto/rea/commit/21097b192b1ab94647e3446b1a2a4dd00e924d5e), [f774dc4](https://github.com/morluto/rea/commit/f774dc420b3befe2b8e68971b4116da2d62fc800), [#6349](https://github.com/morluto/rea/issues/6349), [#4189](https://github.com/morluto/rea/issues/4189). Thanks @coygeek, @hemv-857, @kaoru0822-kitauji, @Nikhi00718.
- **Managed/IL2CPP:** Preserve extension-only scope uncertainty and type discovery after member limits, nested names, ReadyToRun/declaration facts, selected type scope, valid shared-suffix strings, and truncated IL2CPP metadata prefixes. [#5674](https://github.com/morluto/rea/pull/5674), [#5312](https://github.com/morluto/rea/pull/5312), [87fa1b6](https://github.com/morluto/rea/commit/87fa1b613d2b6bda1722855a00f1390f27731b64), [bc2d494](https://github.com/morluto/rea/commit/bc2d494030efd1ccabaeaa0bd49226aa87c378a2), [1f0eb22](https://github.com/morluto/rea/commit/1f0eb228e50908ee2a2aaa72d329178ae1a2cdbd), [4c5fae5](https://github.com/morluto/rea/commit/4c5fae5179dd54c4a0d427cd2a115b9c887e3841), [b3aadaf](https://github.com/morluto/rea/commit/b3aadaf474e59b33e5ff465236008a67eccefb87), [#5467](https://github.com/morluto/rea/issues/5467), [#5146](https://github.com/morluto/rea/issues/5146). Thanks @coygeek, @eco-null, @elricfd, @umi008.
- **Format boundaries:** Preserve independent facts across malformed inputs in model, GPU, WASM, Rust, Android, and SQLite inspection; report optional metadata warnings without discarding core results. [eb41f35](https://github.com/morluto/rea/commit/eb41f358cf83d5626f09de68332f9718f9aa6482), [#5789](https://github.com/morluto/rea/pull/5789), [00a9e95](https://github.com/morluto/rea/commit/00a9e95de91a4bd4d2a41f8aee704a53fa575750), [fc63fe8](https://github.com/morluto/rea/commit/fc63fe8682654086261540dcf3c59afc36c7d58f), [39a91d4](https://github.com/morluto/rea/commit/39a91d4db96ca13224c25936330ea84fe78970e5), [6c6e2ad](https://github.com/morluto/rea/commit/6c6e2ad62b20f810b54f5a567e66fc55241514b2), [929a0a7](https://github.com/morluto/rea/commit/929a0a76c870d9991e015dde58308fd9df0280b8), [d137a4c](https://github.com/morluto/rea/commit/d137a4cdde0049de967f1d5cd429ed9e1f7d0a10), [ef475fc](https://github.com/morluto/rea/commit/ef475fcf8b0601c61052b7091568ba574546417d), [#5749](https://github.com/morluto/rea/issues/5749). Thanks @coygeek, @gensokyo-dev.
- **Capture decoding:** Preserve malformed protobuf trailer facts and wire/schema identity, HTTP/2 URI and UTF-8 failure facts, WebMCP property coverage, and offline capture availability without a browser provider. Report invalid record ordinals as selection errors with actual bounds where available, and trim surrounding SP/HTAB from gRPC-web trailer values while preserving internal whitespace. [9cf7a7c](https://github.com/morluto/rea/commit/9cf7a7cb0d3c72a1121fde280cb11bf911af8055), [b3c3b21](https://github.com/morluto/rea/commit/b3c3b21e43cab4e71b460d08829d8c6e4e9b9c21), [4cb1f1d](https://github.com/morluto/rea/commit/4cb1f1d820df4498ee464bcf72a5f47d40cf8dab), [51e5938](https://github.com/morluto/rea/commit/51e593877642ab42fb5bf25812f4a4110ee7ac26), [d23be5c](https://github.com/morluto/rea/commit/d23be5c0898c39c9f0c54e2e0b43ee746d92d316), [#6725](https://github.com/morluto/rea/pull/6725), [#3947](https://github.com/morluto/rea/issues/3947), [#7067](https://github.com/morluto/rea/pull/7067), [#7068](https://github.com/morluto/rea/issues/7068), [#7069](https://github.com/morluto/rea/pull/7069), [#7071](https://github.com/morluto/rea/issues/7071). Thanks @Adolanium, @coygeek, @Sriram-PR.
- **Evidence and errors:** Authenticate ledger Evidence before committing; preserve parent-schema diagnostics, canonical correction details and owned cleanup outcomes; identify the rejected function-Evidence side/kind. Fixed allowances now name their actual limits. [0407bbf](https://github.com/morluto/rea/commit/0407bbfbe65e8fa475e8bb0c78c30170e2c2f55e), [#6361](https://github.com/morluto/rea/pull/6361), [1629266](https://github.com/morluto/rea/commit/1629266e3c08c90fed6bba5383528af7986a12b7), [#6969](https://github.com/morluto/rea/pull/6969), [#5860](https://github.com/morluto/rea/pull/5860), [#6034](https://github.com/morluto/rea/issues/6034), [#5932](https://github.com/morluto/rea/issues/5932), [#5847](https://github.com/morluto/rea/issues/5847). Thanks @AyoubElleuch, @coygeek, @dhruvac29, @kaoru0822-kitauji.
- **ADB and firmware recovery:** Report an unparsed push checksum as unverified, normalize absent installer-package sentinels, describe extraction-relative Unblob links, and avoid recommending a budget increase at its fixed ceiling. [#6010](https://github.com/morluto/rea/pull/6010), [#4073](https://github.com/morluto/rea/pull/4073), [#4573](https://github.com/morluto/rea/pull/4573), [#6971](https://github.com/morluto/rea/pull/6971), [#5924](https://github.com/morluto/rea/issues/5924), [#4175](https://github.com/morluto/rea/issues/4175). Thanks @coygeek, @Ganesh2325, @kaoru0822-kitauji, @Rinkia, @XiaoBinGan, @yankikucuk.
- **CLI formatting:** Share Markdown report formatting, exclude inherited row values, and preserve arrays of empty objects as JSON. Render Markdown table headers and cells literally while keeping intraword underscores readable. [b784912](https://github.com/morluto/rea/commit/b784912811fd10ac9dc425696dc0e9af632afa34), [#6984](https://github.com/morluto/rea/pull/6984), [#7058](https://github.com/morluto/rea/pull/7058), [#7057](https://github.com/morluto/rea/issues/7057), [#7073](https://github.com/morluto/rea/pull/7073). Thanks @Plastic-time, @ShuaixinHuang, @viniciusdsandrade.
- **Format identity:** Preserve Python marshal scalar/decode limits, empty optional BEAM chunks, PE resource branch identities and certificate alignment, unsupported compressed ELF notes, and codec-framed GPU bundle extents. [0603223](https://github.com/morluto/rea/commit/06032237c6755e3dd5f9a8db732c89958b238cc7), [c46064c](https://github.com/morluto/rea/commit/c46064c3f6eaba121c5de7b17390bfdecdd132ef), [66671e8](https://github.com/morluto/rea/commit/66671e874888e98f7e2b27e68a7c6fb320bbb827), [03077d5](https://github.com/morluto/rea/commit/03077d54afc49613db4e38f70c506bc60f5223aa), [#6721](https://github.com/morluto/rea/pull/6721), [9fb8c11](https://github.com/morluto/rea/commit/9fb8c114a03ad60e57c11586967c6df845a3f97e), [f6a1e2c](https://github.com/morluto/rea/commit/f6a1e2c892dc23c7583c3d03af9edb01632ff5a5), [#6694](https://github.com/morluto/rea/issues/6694). Thanks @coygeek, @Deepak-Kambala.
- **WASM and APK metadata:** Preserve ambiguous WASM identity and partial facts, glue URL query/fragment text across line terminators, APK signer identity, and raw issuer identity when comparison rules are unknown. [af8f0ba](https://github.com/morluto/rea/commit/af8f0badf64fd3546392b6aa4625777904b2f051), [#6802](https://github.com/morluto/rea/pull/6802), [f783f0f](https://github.com/morluto/rea/commit/f783f0f1e82472cdddd4de2156d974f729e332a6), [115a78f](https://github.com/morluto/rea/commit/115a78fe241276c34917b496621954c5f29bfc15), [#6639](https://github.com/morluto/rea/issues/6639). Thanks @coygeek, @gensokyo-dev, @gongfpp.
- **Source identity:** Preserve Next route spelling, module grammar and loader request identity, directory package-main behavior, accessor effects, exact source targets, and resolver-discovery versus module-loading distinctions. [ddd7ac7](https://github.com/morluto/rea/commit/ddd7ac7b846b48191ead1f7dcd0e3bd735468fa8), [9974512](https://github.com/morluto/rea/commit/99745126009b15060d71c38809418858a07d5afa), [f2e5fd1](https://github.com/morluto/rea/commit/f2e5fd148a85fa737302dd194a33172c4561093e), [baf1678](https://github.com/morluto/rea/commit/baf16784af3592534687052431c145daef55cfd1), [6e324b2](https://github.com/morluto/rea/commit/6e324b224405f2befd953234f6dc210cf72ad50a), [75daf0c](https://github.com/morluto/rea/commit/75daf0c7df108f04ab41f34aec1a31d4a7ceb322), [f91b7f8](https://github.com/morluto/rea/commit/f91b7f8009c9af27a5e0148e3b07d20bfe3b7a2a).
- **Graphs and selected facts:** Preserve JavaScript graph omissions/version facts, Electron native-loader facts, web JSON identity/coverage, module-path selection, feature-trace qualifications, selected native paths, endpoint context, and mixed Swift demangler outcomes. [30db499](https://github.com/morluto/rea/commit/30db499db310f073d9bf3d3acfd77985e1cb7e2e), [5a7bddb](https://github.com/morluto/rea/commit/5a7bddb3aeb0345854e76b79d529aed1c40e2054), [79805f2](https://github.com/morluto/rea/commit/79805f244a1ec0850f3109929168a9c8ebb1b908), [f719dc4](https://github.com/morluto/rea/commit/f719dc46c51a55b3926eec9659d06e7dfb95132b), [7784fdb](https://github.com/morluto/rea/commit/7784fdbad78f8b384d513f0e7dae8e983cd48ef8), [76500f1](https://github.com/morluto/rea/commit/76500f12b54e07b1ea700a403b2593890d63b42a), [676d29f](https://github.com/morluto/rea/commit/676d29f9eef794cea668f5f09f0ff510c848a513), [edf7b3d](https://github.com/morluto/rea/commit/edf7b3d32c3248f221e27f7ce5a3d34e8590285b), [93de7bc](https://github.com/morluto/rea/commit/93de7bc7e324a761162a31625b838ba0d4e3824d).
- **Managed boundaries:** Honor native platform symbol lookup, consume resolved IL2CPP ELF declarations, and attribute malformed comparison inputs to the supplied argument. [64fa1db](https://github.com/morluto/rea/commit/64fa1db1309218283df19f3ee4dba61f5fbd2a67), [87745af](https://github.com/morluto/rea/commit/87745af52ab0ccd3a172c2534c516d41b0095035), [96ad94f](https://github.com/morluto/rea/commit/96ad94f69eda5d68c7335ff775b6927b1376fd2b).
- **Provider and worker recovery:** Preserve output loss after successful command exit, physical refusal diagnostics, bounded worker control framing, JavaScript worker-phase/budget attribution, admitted ambiguity choices, and selected EVM limiter availability. Scope objdump debug-link limitations to DWARF requests and keep process-capture preparation nonexecuting. [e639edf](https://github.com/morluto/rea/commit/e639edf6204c05285d96840478926592daf95876), [171b33b](https://github.com/morluto/rea/commit/171b33b2c4defc6cbea49545bedf4272b72d1ea2), [34563fc](https://github.com/morluto/rea/commit/34563fcc9bd92af30668820dcac5635e27181094), [3f9357b](https://github.com/morluto/rea/commit/3f9357b5990d166a38440f56f439452266650093), [cec8f1e](https://github.com/morluto/rea/commit/cec8f1e8f305d6332272457a6ee419d3c5fb6359), [a74d859](https://github.com/morluto/rea/commit/a74d859a19122fb8f1282b3632d320997b1caae0), [c284168](https://github.com/morluto/rea/commit/c284168c4b1cd214ce7f504d43da28528d82d603), [4fb936f](https://github.com/morluto/rea/commit/4fb936fe86bb7a07f94f819ae8510301f2ef69c9).
- **Setup and limits:** Retain unknown environment-budget facts, selected-skill/maintenance uncertainty, Hopper URL admission and bookmark verification, and listener failure recovery that matches the system cause. [a718037](https://github.com/morluto/rea/commit/a7180370332350bc0f998fbd643888221759570e), [d1fbebb](https://github.com/morluto/rea/commit/d1fbebb578538dc94fd5e376e6e1959218efa835), [60accad](https://github.com/morluto/rea/commit/60accad2de8f2862fe91901502fe26cc5390fb36), [fd7da77](https://github.com/morluto/rea/commit/fd7da77fc02dcfaf7d1bc9f2fb240d71cdd27c59), [0c440bf](https://github.com/morluto/rea/commit/0c440bfc07f3cb343e2e836d27cccaed539cfe9c).
- **State and Evidence:** Retain export tarball ownership and SQLite primary outcomes through cleanup retry, count emitted SQLite schema names across encodings, scope display refresh to matching snapshot bindings, and report retained-result validation failures consistently. Reconstruction uses one effective completion scope and kind-based labels when names are blank. [ab58edc](https://github.com/morluto/rea/commit/ab58edcdf50b4bd687900ee2dc362ecc5c3dce15), [1006714](https://github.com/morluto/rea/commit/100671427105791a5c5913a918b63a394f6bc83e), [a47788a](https://github.com/morluto/rea/commit/a47788a6a4abb067273f954275a9d0a269adf1e7), [8aa40e9](https://github.com/morluto/rea/commit/8aa40e9a61155cff8b6f3c138c601bf805b20653), [b8c1b45](https://github.com/morluto/rea/commit/b8c1b45316882beeab4a9de49381d94e131fab0f), [a35b933](https://github.com/morluto/rea/commit/a35b9335a0124674ee5e3abe8c075f1fff6b4fa6), [#6137](https://github.com/morluto/rea/pull/6137), [#6126](https://github.com/morluto/rea/issues/6126). Thanks @carlogiovannineri-rgb, @mohithhhh.
- **Capture coverage:** Name browser bundle budgets and deadline recovery, separate domain readiness from worker attribution, attribute rendered-frame omissions to their producing stage, share offline-capture host admission with discovery, preserve selected signal-file semantics, and retain short Flutter GNU build IDs. [#6036](https://github.com/morluto/rea/pull/6036), [ba78133](https://github.com/morluto/rea/commit/ba781330d6d28b22d15498e63c144fb40e086156), [5934387](https://github.com/morluto/rea/commit/5934387f4185129efe9dc8b7338d8bc3fa5b25bd), [ad629b2](https://github.com/morluto/rea/commit/ad629b2e31c3a9495b27179360b508fa021fe86a), [928ba76](https://github.com/morluto/rea/commit/928ba76882bfd216dde8ed79118789b2c3aad4f9), [678c4cf](https://github.com/morluto/rea/commit/678c4cfa68e2fee12f620075e12dc87d9b1195b0).
- **Additional boundary facts:** Retain parser format/query occurrence identity and independent malformed-input facts, unresolved nested Swift identifiers, and completed signing-capture coverage; describe crash documents without assuming a provider. [5f702dd](https://github.com/morluto/rea/commit/5f702dd6ccd9e8e505e224854b91678439c3b41f), [99d2d7a](https://github.com/morluto/rea/commit/99d2d7ad22bcbd780919760008aba6a88e7d1077), [90e433d](https://github.com/morluto/rea/commit/90e433df06b3f83417176b8db1b0a905f9df1d3a), [#5548](https://github.com/morluto/rea/pull/5548), [ee12d51](https://github.com/morluto/rea/commit/ee12d517a25d34853d40e31fc039c7d1576c60e1), [#5538](https://github.com/morluto/rea/issues/5538). Thanks @coygeek, @ShuaixinHuang.
- **Documentation:** Add the Romanian README. [#7026](https://github.com/morluto/rea/pull/7026). Thanks @An3treiu.
Full contribution record: [1740 commits and 497 merged PRs](https://github.com/morluto/rea/blob/rea-agents-6.8.0/docs/releases/6.8.0.contributions.json).
## [6.7.0](https://github.com/morluto/rea/compare/rea-agents-6.6.0...rea-agents-6.7.0) (2026-10-18)
### Highlights
+18 -10
View File
@@ -15,6 +15,11 @@ import sys
PROFILE = "pwntools@4.15.0;pyelftools@0.33;unicorn@2.1.2"
PACKAGES = {"pwntools": "4.15.0", "pyelftools": "0.33", "unicorn": "2.1.2"}
OUTPUT_BYTES = 64 * 1024 * 1024
# (e_machine, ELF class bits, byte order) -> architecture pwntools must also report.
PROFILES = {(62, 64, "little"): "amd64", (3, 32, "little"): "i386", (40, 32, "little"): "arm",
(8, 32, "little"): "mips", (8, 32, "big"): "mips"}
PROFILE_SCOPE = ("Layout profile requires ELF64 x86-64, ELF32 i386, ELF32 ARM little-endian "
"or ELF32 MIPS little- or big-endian objects.")
class LayoutFailure(Exception):
@@ -116,13 +121,16 @@ def inspect_elf(path, cache):
raise LayoutFailure("format", "Selected object does not contain an ELF header.")
if len(content) < 16:
raise LayoutFailure("format", "Selected ELF identification is truncated.")
if content[4] != 2 or content[5] != 1:
raise LayoutFailure("unsupported-target", "Initial layout profile requires ELF64 little-endian objects.")
if len(content) < 64:
raise LayoutFailure("format", "Selected ELF64 header is truncated.")
if int.from_bytes(content[18:20], "little") != 62:
raise LayoutFailure("unsupported-target", "Initial layout profile requires x86-64 ELF objects.")
if int.from_bytes(content[16:18], "little") not in (1, 2, 3):
if content[4] not in (1, 2) or content[5] not in (1, 2):
raise LayoutFailure("unsupported-target", "Selected ELF declares an unknown class or byte order.")
bits, header_size = (32, 52) if content[4] == 1 else (64, 64)
order = "little" if content[5] == 1 else "big"
if len(content) < header_size:
raise LayoutFailure("format", f"Selected ELF{bits} header is truncated.")
profile = (int.from_bytes(content[18:20], order), bits, order)
if profile not in PROFILES:
raise LayoutFailure("unsupported-target", PROFILE_SCOPE)
if int.from_bytes(content[16:18], order) not in (1, 2, 3):
raise LayoutFailure("unsupported-target", "Selected ELF type is outside the EXEC/DYN/REL layout profile; recorded core analysis is separate.")
from pwnlib.context import context
with context.local(cache_dir=str(cache), log_level="warning"):
@@ -185,8 +193,8 @@ def inspect_elf(path, cache):
if type is None or section.header.sh_type == type)
try:
image = LayoutELF(str(path), checksec=False)
if image.arch != "amd64" or image.bits != 64 or image.endian != "little":
raise LayoutFailure("unsupported-target", "Initial layout profile requires x86-64 ELF64 little-endian objects.")
if (image.arch, image.bits, image.endian) != (PROFILES[profile], bits, order):
raise LayoutFailure("unsupported-target", PROFILE_SCOPE)
if image.header.e_type not in ("ET_EXEC", "ET_DYN", "ET_REL"):
raise LayoutFailure("unsupported-target", "Selected ELF type is outside the EXEC/DYN/REL layout profile; recorded core analysis is separate.")
length = len(content)
@@ -208,7 +216,7 @@ def inspect_elf(path, cache):
})
if h.sh_type in ("SHT_SYMTAB", "SHT_DYNSYM"):
if h.sh_entsize < image.structs.Elf_Sym.sizeof():
raise LayoutFailure("format", "ELF symbol entry is smaller than the decoded Elf64_Sym structure.")
raise LayoutFailure("format", f"ELF symbol entry is smaller than the decoded Elf{bits}_Sym structure.")
if h.sh_link >= len(all_sections) or all_sections[h.sh_link].header.sh_type != "SHT_STRTAB":
raise LayoutFailure("format", f"ELF symbol table {index} links to invalid string table {h.sh_link}.")
strings = all_sections[h.sh_link]
+4 -1
View File
@@ -31,7 +31,10 @@ stackshots, return no crash result.
`process`, `exception`, `termination`, `faulting_thread`, `threads`, and
`images` reproduce documented body fields when present, including each image's
`name` and each frame's `symbolLocation` (as `symbol_location`, the byte offset
from the start of `recorded_symbol`). Integers keep their
from the start of `recorded_symbol`). `termination` keeps the terminating
process as `by_proc` and `by_pid` and its `flags`; `process.parent_pid` keeps
`parentPid` next to `parent`; and `metadata` keeps `build_version` and
`platform`. Integers keep their
decimal literals and a hexadecimal form, including arm64e registers above
2^53. Frame roles follow the recorded frame index: row zero of the resolved
faulting thread is `faulting_instruction`, row zero of other threads is
+6 -3
View File
@@ -8,9 +8,12 @@ ordinary binary inspection use the same modular capability.
## Bring your own engine
Initial real verification covers **Linux x64, ELF64 x86-64 little-endian
EXEC/DYN/REL**. Provide an absolute Python executable whose environment already
contains unchanged pwntools 4.15.0, pyelftools 0.33 and Unicorn 2.1.2:
Real verification covers **Linux x64** hosts and **EXEC/DYN/REL** objects in
these profiles: ELF64 x86-64, ELF32 i386, ELF32 little-endian ARM and ELF32
little- or big-endian MIPS. Other machines, classes and byte orders, including
AArch64 and big-endian ARM, return `unsupported_target`. Provide an absolute
Python executable whose environment already contains unchanged pwntools 4.15.0,
pyelftools 0.33 and Unicorn 2.1.2:
```sh
REA_PWNTOOLS_PYTHON=/absolute/isolated-env/bin/python \
+45
View File
@@ -79,6 +79,51 @@ profile for this request; it does not start your fixture server or install a
browser. A schema-valid scenario does not prove that the executable or target
is available on the host.
### Validate without a browser
Add `--dry-run` to check a scenario before it launches or changes anything:
```bash
rea capture-browser-scenario ./browser-scenario.json --dry-run --json
```
The dry run reads the same JSON argument and resolves the same paths as a
capture, then applies the same schema and semantic rules: duplicate or reserved
step IDs, duplicate secret IDs or variables, and undeclared secret references.
It also requires each declared secret's environment variable to be set, which
is the rule a capture applies before acquiring a browser. Errors are the same
`invalid_request` projection with field paths, under
`"error": "Browser scenario validation failed"`. A valid scenario exits 0
with a compact plan and creates no provider, profile, file or connection
(output shown for the minimal example above):
```text
{
"operation": "capture_browser_scenario",
"dry_run": true,
"browser_mode": "launch",
"start_origin": "http://127.0.0.1:3000",
"steps": [
{ "step_id": "scenario_start", "action": "goto_start" },
{ "step_id": "settle", "action": "wait_for_timeout" }
],
"secrets": [],
"not_checked": [
"browser executable, CDP endpoint and target discovery",
"browser launch or attachment",
"navigation, network reachability and page state",
"locator resolution, action outcomes and capture limits"
]
}
```
Steps are listed in capture order, including the initial navigation. Secrets
report only `environment_value: "present"`; values are never read into the
plan or printed. `not_checked` names the stages a dry run cannot establish, so
a valid plan is not a prediction that the capture will succeed. The dry run is
CLI-only. Over MCP, `capture_browser_scenario` already rejects invalid input
before acquiring a browser.
## Interaction example: capture the visible result
The default final URL answers "where did the page end up?", not "what did the
+4 -2
View File
@@ -924,8 +924,10 @@ redirected, which keeps large results smaller for agents and scripts; pipe
through `jq .` for indented files. Output selection and formatting do not change
operation status.
`--format md` escapes backslashes and pipes in table headers and cells, and
renders cell line breaks as `<br>`. These are presentation changes; retained
`--format md` escapes backslashes, pipes, and inline Markdown syntax in table
headers and cells so source text is displayed literally, and renders cell line
breaks as `<br>`. Underscores between letters or digits, as in `source_index`,
cannot form emphasis and stay unescaped. These are presentation changes; retained
Evidence and JSON output keep the original values. Nested objects and arrays
use JSON inside their owning Markdown cells, retaining the field names and
values instead of replacing them with an object label. Table widths are measured
+1 -1
View File
@@ -15,7 +15,7 @@ snapshot parser and a separate optional annotation extension. Check the
[release boundary](installation.md#released-package-and-main) when using npm.
See [import language and analysis seeds](installation.md#ghidra-import-language-and-analysis-seeds)
to override the header-detected language or seed analysis. An override that conflicts with a fixed raw, DOS, PSP, MIPS or RISC-V import
to override the header-detected language or seed analysis. An override that conflicts with a fixed raw, DOS, PSP, MIPS, RISC-V or GameCube DOL import
is a configuration error naming the conflicting settings. It does not make an
otherwise admitted target unsupported; REA preserves the requested settings.
An inaccessible language
+5 -5
View File
@@ -319,14 +319,14 @@ Register with the variable unset:
<!-- x-release-please-start-version -->
```bash
claude mcp add --scope local rea -- npx -y rea-agents@6.7.0 mcp
claude mcp add --scope local rea -- npx -y rea-agents@6.8.0 mcp
```
On native Windows, use the same project directory and wrap the package-runner
shim so the stdio client can launch it:
```powershell
claude mcp add --scope local rea -- cmd.exe /d /c npx -y rea-agents@6.7.0 mcp
claude mcp add --scope local rea -- cmd.exe /d /c npx -y rea-agents@6.8.0 mcp
```
<!-- x-release-please-end -->
@@ -620,8 +620,8 @@ hosted connector; see [its configuration](client-configuration.md#other-client-s
`droid`, `dsh`, `kimi`, `qoder`, `trae`, `workbuddy`, and `zcode` require
`rea-agents@6.5.0` or later. The 6.4.0 registry check above does not cover them.
`freebuff` is not in a release yet - `6.7.0` is the latest - so it also needs a
release containing it.
`freebuff` requires `rea-agents@6.8.0` or later. Older releases reject
`rea setup --client freebuff` as an unsupported client.
See [client configuration and troubleshooting](client-configuration.md) for
configuration paths, profiles, trust policies, and schema-limit workarounds.
@@ -1471,7 +1471,7 @@ For a client that requires manual configuration, use:
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@6.7.0", "mcp"]
"args": ["-y", "rea-agents@6.8.0", "mcp"]
}
}
}
File diff suppressed because it is too large. Load diff
+7
View File
@@ -112,6 +112,13 @@ creation or retry mutations. An internal GraphQL error does not establish API
quota exhaustion or prove that query size caused the failure. Publication
continues through the pinned release-please action.
A nonzero manifest version must resolve to a GitHub release or tag before
proposal history is collected. If publication has not created that boundary
yet, preparation stops before opening or updating a PR; wait for publication
before retrying. Missing manifest versions and `0.0.0` still allow an initial
release. This prevents historical `Release-As` directives from being replayed
as unreleased changes.
When upgrading release-please, review the upstream manifest implementation,
refresh the versioned patch and run the release proposal and workflow boundary
tests. Version mismatch or a patch that neither applies cleanly nor matches an
@@ -0,0 +1,8 @@
# Implementation plan
1. **Read-backs.** A create's observation looks for the first read of its entity before the next write; an item read's
fields and clock spellings are taken when it agrees otherwise, and the read observes; a list read adds fields only. The plan's observation records the
read and its shape, and serving merges the same objects.
2. **Sent members.** An update's sent member stored as a fresh clock after it sent back the held one is a `clock`; one
stored as the slug of another member stored as sent is a `slug`.
3. **Schema, tests and docs.**
@@ -0,0 +1,47 @@
# Writes whose answers show the entity otherwise than reads, and whole-object updates
Issue: https://github.com/morluto/rea/issues/4933
## Outcome
Agents built environments from a prompt for five applications the prompt flow had never seen (Vikunja, Ghost, EspoCRM,
Paperless-ngx and Miniflux). Two write-model gaps withheld most of their creates and edits:
- **A create's answer is another view of the entity than its reads.** Paperless answers a tag create without the
`document_count` its item read shows. EspoCRM answers a contact create without nine fields its item read shows.
Vikunja answers a label create with a partial `created_by` and clocks to the nanosecond, where reads show the full
user and whole seconds. Ghost answers a tag create without the `count` its list shows. The state built the entity from
the answer alone, so the read-back contradicted the create.
- **A whole-object update sends back what the application derives.** Paperless's UI sends the tag's old `slug` with a
new name, and the application slugs the name anew. Ghost's UI sends the `updated_at` it read, and the application
stamps a new one. The update was withheld as `unexplained-change`, or its first call's value was pinned so that later
calls conflicted.
## Behaviour
- **Read-backs.** When the first read of a created entity before the next write shows fields the create's answer lacks,
or spells its clocks to another precision, the observed entity takes them, and the plan records the read
(`observation.read`, with its shape).
- An item read that agrees with the answer otherwise lends its fields and its clock spellings (whole seconds of a
nanosecond clock, or microseconds of one), and no longer confirms the create. One that disagrees on another value,
or omits an answer field its item view renders, still checks the create, which it contradicts.
- A list read, which may show fewer keys of an object, only adds fields the answer lacks and still checks the create.
- A read that shows nothing new leaves the observation as it was and still confirms the create.
- Serving merges the same two recorded objects, so a served create answers in its answer's shape and stores the
fields of both. Generated clocks in the create answer retain that answer's recorded precision independently of
the stored read-back precision.
- **Restamped clocks.** An update member sent back as the clock the entity held, and stored as a fresh clock near the
call, is a `clock` rule, accepted with any value.
- **Re-derived slugs.** An update member stored as the slug of another member the call stored as sent is a `slug` rule
from that member, accepted with any value.
- **Compatible saves.** A call that echoes an already-derived slug or holds a clock in its own recorded second does
not conflict with a demonstrated derivation, in either call order. A slug derivation must reproduce earlier saves
as well; an explicit custom slug that disagrees still withholds the update.
Plans and views that need none of this are unchanged.
## Acceptance
- The held-out recordings serve EspoCRM's contact create and Paperless's tag edit.
- A create whose item read-back disagrees with its answer on another value is still contradicted.
- No write tool of the 69 recorded captures goes from served to withheld.
@@ -0,0 +1,8 @@
# Tasks
- [x] Take a create's read-back fields into its observation, and serve the same merge.
- [x] Learn restamped clocks and re-derived slugs for sent update members.
- [x] Add view and serving tests that fail without the change.
- [x] Document the observation member and the rules.
- [x] Measure the held-out and recorded captures.
- [x] Run static, formatting, documentation and test gates.
@@ -0,0 +1,48 @@
# Verification
Executed on macOS with Node v24.22.1 at `2f807efb` (main `8e6a9578` and this branch).
## Gates
- `npm run check` (build, typecheck, format, lint, typed lint, module boundaries, architecture guards) and
`npm run docs:check`: passed.
- `npx vitest run --project domain --project services --project adapters`: 17,624 passed, 25 skipped, 1 failed ("stops
Inspector target authorization after the current stat settles"), which fails the same way on main in this environment.
- `npm run test:focused --` the analysis-view, environment build, compare, export and serve CLI boundaries, the
analysis-view, environment bundle, serve and served-writes MCP boundaries, tool schema validity, output schema profile
and MCP schema validation: 12 files, 106 passed.
- The eight new view and serving tests fail on main and pass here.
## Recorded captures
The 69 recorded captures and the five held-out agent recordings, built as environment-state views on main and on this
branch:
| Captures | Served on main | Served here | Served to withheld |
| ----------------------------------------- | -------------- | ----------- | ------------------ |
| 69 recorded | 132 | 132 | none |
| 5 held-out (20 write tools with a target) | 8 | 10 | none |
- **Now served:** EspoCRM's contact create (its read-back adds nine fields the answer lacks) and Paperless's tag edit
(the stale slug is slugged anew from the name).
- **Past the read-back, withheld by other gaps:**
- Paperless's tag create: its list's `all` member lists every id, which state rendering does not model.
- Ghost's tag create and edit: the state does not list the created tags in Ghost's tag lists.
- **Still contradicted:** Vikunja's label create, whose item read-back shows a fully loaded `created_by` where the answer
showed a partial one.
- Three plans of the recorded captures (Airbyte, BookStack, PhotoPrism) now record a read-back; their served state is
unchanged.
## Served write tasks
The held-out round's write tasks, rebuilt from the same agent recordings with this branch, with
`fireworks_ai/gpt-oss-120b` and `fireworks_ai/glm-5p3-flash`:
| Application | Main (gpt-oss, GLM) | This branch (gpt-oss, GLM) |
| ---------------------------------------------- | ------------------- | -------------------------- |
| EspoCRM contacts: create, update email, delete | 0/3, 0/3 | 1/3, 1/3 (create) |
| Paperless tags: create, rename, delete | 1/3, 1/3 | 1/3, 2/3 (rename with GLM) |
- gpt-oss's rename did not apply. Its trajectory was not kept, so the cause is unconfirmed. A served `PUT` requires
every member the recorded call sent (about 14 here), and GLM sent them all.
- EspoCRM's served update accepts only the members its recording edited, so the email change is refused, as on main.
+6 -2
View File
@@ -2008,12 +2008,16 @@ coverage limits.
### Offline binary layout
`npm run verify:binary:layout` requires Linux x64, GCC/binutils, absolute
`npm run verify:binary:layout` requires Linux x64, GCC/binutils, the
`i686-linux-gnu`, `arm-linux-gnueabi`, `mips-linux-gnu` and `mipsel-linux-gnu`
cross `gcc`/`readelf` toolchains, absolute
`REA_PWNTOOLS_PYTHON` with pwntools 4.15.0/pyelftools 0.33/Unicorn 2.1.2 and
absolute `REA_VERIFY_STRACE_COMMAND`. It compiles ephemeral source-owned ELF
fixtures and checks public CLI/MCP, lossless addresses/names, file ranges,
mitigation inferences, malformed/unsupported input, original file hashes and
released process ownership. Exec syscall tracing must identify only the declared
released process ownership. Cross-compiled ELF32 executables and relocatable
objects check the reported class, machine and byte order, and every section
name, address, offset and size against GNU `readelf`. Exec syscall tracing must identify only the declared
Node/Python launchers; no target binary is executed. Core/debugger claims need
separate verification lanes. Pass an installed package entrypoint as the script's
first argument to verify packaging independently of the checkout.
+18 -2
View File
@@ -746,7 +746,16 @@ integer of 10 or more digits that no entity of the type holds (where every one
holds a distinct integer of the same length, as time-based ids do), a
`nested-clock` object whose clocks the request stamped, a `template` around
a generated value, a `reference` to an entity, or a `composed` string; other
fields keep the observed value. A `reference` is an object holding fields of the
fields keep the observed value. When the first read of a created entity before
the next write shows fields the create's answer lacks, or its clocks to another
precision, the observed entity takes them (`observation.read`). An item read
that agrees with the answer otherwise lends its fields and clock spellings and
no longer confirms the create; one that disagrees or omits an answer field its
item view renders still checks it. Clock agreement uses the coarser recorded
precision, including microseconds. A list read
only adds fields the answer lacks and still checks it. Serving answers a create
in its answer's shape and clock precision and stores the fields of both with
the item read-back's clock precision. A `reference` is an object holding fields of the
entity a sent member names, as the state holds it (a parent beside `parentId`):
the member is accepted with any value, serving looks the entity up and refuses a
call naming one the environment does not hold or holds in more than one
@@ -778,7 +787,14 @@ with them. Other clocks the plan stamps take that spelling too unless their own
value shows a fixed width. A body that wraps one
object in a one-element array is that object, wrapped so on every call
(`body.envelope`). An update's members
echo when stored as sent; a member stored differently is accepted only with its
echo when stored as sent. A clock sent back as the entity held it and stored as
a fresh clock near the call is a `clock`, and a member stored as the slug of
another member stored as sent is a `slug`, both accepted with any value, as a
whole-object write that sends what it read shows. A save that echoes an
already-derived slug or holds a clock in its own second remains compatible
with that derivation in either call order; earlier saves must also reproduce
a newly learned slug. Any other member stored
differently is accepted only with its
recorded value (`body.fixed`) and stores the value the call that showed it
recorded (the `value` of its `constant` rule), and a field it changed without
sending must be a clock, a nested clock, an increment, a slug, a fresh `unique`
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "rea-agents",
"version": "6.7.0",
"version": "6.8.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "rea-agents",
"version": "6.7.0",
"version": "6.8.0",
"license": "MIT",
"dependencies": {
"@babel/parser": "8.0.4",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "rea-agents",
"mcpName": "io.github.morluto/rea",
"version": "6.7.0",
"version": "6.8.0",
"description": "Reverse engineer anything from your terminal or agent with one CLI and MCP server.",
"license": "MIT",
"repository": {
+19 -4
View File
@@ -105,7 +105,22 @@
}
--- a/build/src/manifest.js
+++ b/build/src/manifest.js
@@ -261,9 +261,20 @@
@@ -248,6 +248,14 @@
releasesFound++;
}
}
+ // A recorded version requires its release boundary, even while a
+ // concurrent publisher has not created the matching release/tag yet.
+ for (const path of Object.keys(strategiesByPath)) {
+ const expectedVersion = this.releasedVersions[path];
+ if (expectedVersion && expectedVersion.toString() !== '0.0.0' && !releasesByPath[path]) {
+ throw new Error(`Missing expected release for path ${path} at version ${expectedVersion.toString()}: no matching release or tag found. Refusing to bootstrap an already released package.`);
+ }
+ }
const needsBootstrap = releasesFound < expectedReleases;
if (releasesFound < expectedReleases) {
this.logger.warn(`Expected ${expectedReleases} releases, only found ${releasesFound}`);
@@ -261,9 +269,20 @@
this.logger.info('Collecting commits since all latest releases');
const commits = [];
this.logger.debug(`commit search depth: ${this.commitSearchDepth}`);
@@ -127,7 +142,7 @@
batchSize: this.commitBatchSize,
});
const releaseShas = new Set(Object.values(releaseShasByPath));
@@ -272,6 +283,7 @@
@@ -272,6 +291,7 @@
// sha => release pull request
const releasePullRequestsBySha = {};
let releaseCommitsFound = 0;
@@ -135,7 +150,7 @@
for await (const commit of commitGenerator) {
if (releaseShas.has(commit.sha)) {
if (commit.pullRequest) {
@@ -284,14 +296,17 @@
@@ -284,14 +304,17 @@
}
if (this.lastReleaseSha && this.lastReleaseSha === commit.sha) {
this.logger.info(`Using configured lastReleaseSha ${this.lastReleaseSha} as last commit.`);
@@ -153,7 +168,7 @@
break;
}
commits.push({
@@ -301,16 +316,18 @@
@@ -301,16 +324,18 @@
pullRequest: commit.pullRequest,
});
}
+1 -1
View File
@@ -104,7 +104,7 @@ export async function readReleaseHistory(run, base, target) {
// GitHub maps these coding-agent trailers to ordinary User actors.
// Keep those source identities in the inventory while excluding automation credit.
const AGENT_ACCOUNTS = new Set(["claude", "cursoragent", "codex"]);
const AGENT_ACCOUNTS = new Set(["claude", "cursoragent", "codex", "copilot"]);
// REA's maintainer requested external-contributor thanks without self-credit.
const MAINTAINER_ACCOUNTS = new Set(["morluto"]);
const human = (value) =>
+175 -2
View File
@@ -35,11 +35,27 @@ if (
!isAbsolute(strace ?? "")
)
throw new Error(
"verify:binary:layout requires Linux x64, absolute REA_PWNTOOLS_PYTHON (pwntools 4.15.0/pyelftools 0.33/Unicorn 2.1.2) and REA_VERIFY_STRACE_COMMAND. gcc, ld and strip must be available. No target is executed.",
"verify:binary:layout requires Linux x64, absolute REA_PWNTOOLS_PYTHON (pwntools 4.15.0/pyelftools 0.33/Unicorn 2.1.2) and REA_VERIFY_STRACE_COMMAND. gcc, ld, strip and the i686/ARM/MIPS/MIPSel linux-gnu cross gcc and readelf must be available. No target is executed.",
);
const run = createVerifierRun();
const execute = promisify(execFile);
for (const command of ["gcc", "ld", "strip", "/usr/bin/prlimit", strace]) {
const ELF32_TOOLCHAINS = [
["i686-linux-gnu", "EM_386", "little"],
["arm-linux-gnueabi", "EM_ARM", "little"],
["mips-linux-gnu", "EM_MIPS", "big"],
["mipsel-linux-gnu", "EM_MIPS", "little"],
];
for (const command of [
"gcc",
"ld",
"strip",
"/usr/bin/prlimit",
strace,
...ELF32_TOOLCHAINS.flatMap(([prefix]) => [
`${prefix}-gcc`,
`${prefix}-readelf`,
]),
]) {
try {
await execute(command, [command === strace ? "-V" : "--version"], {
timeout: 10_000,
@@ -328,6 +344,163 @@ try {
cases++;
}
}
// ELF32 profiles: cross-compiled, never executed, checked against GNU readelf.
for (const [
prefix,
machine,
byteOrder,
kind,
flags,
] of ELF32_TOOLCHAINS.flatMap((toolchain) => [
[...toolchain, "executable", ["-fPIE", "-pie"]],
[...toolchain, "relocatable", ["-c", "-fPIC"]],
])) {
const path = join(root.path, `${prefix}-${kind}`);
await execute(`${prefix}-gcc`, ["-O1", ...flags, source, "-o", path], {
timeout: 30_000,
maxBuffer: 1024 * 1024,
});
const { stdout } = await execute(`${prefix}-readelf`, ["-SWsr", path], {
timeout: 10_000,
maxBuffer: 16 * 1024 * 1024,
env: { ...process.env, LC_ALL: "C" },
});
const expectedSections = [
...stdout.matchAll(
/^\s*\[\s*(\d+)\]\s+(\S+)\s+\S+\s+([0-9a-f]+)\s+([0-9a-f]+)\s+([0-9a-f]+)\s+[0-9a-f]+\s+[A-Za-z]*\s+(\d+)\s+\d+\s+\d+\s*$/gm,
),
]
.filter(([, index]) => index !== "0")
.map(([, index, name, address, offset, size, link]) => ({
index: Number(index),
name,
address: BigInt(`0x${address}`),
offset: BigInt(`0x${offset}`),
size: BigInt(`0x${size}`),
link: Number(link),
}));
assert.ok(
expectedSections.length > 0,
`readelf listed no sections for ${path}`,
);
const expectedSymbols = [];
for (const [, name, count, body] of stdout.matchAll(
/^Symbol table '([^']+)' contains (\d+) entr(?:y|ies):\n((?:[^\n]+\n?)*)/gm,
)) {
const table = expectedSections.find((section) => section.name === name);
assert.notEqual(table, undefined, `readelf symbol table absent: ${name}`);
const rows = [...body.matchAll(/^\s*(\d+):\s+([0-9a-f]+)\s+(\d+)\s/gm)];
assert.equal(rows.length, Number(count), `readelf symbol count: ${name}`);
for (const [, index, value, size] of rows)
expectedSymbols.push({
table_index: table.index,
entry_index: Number(index),
value: BigInt(`0x${value}`),
size: BigInt(size),
});
}
assert.ok(
expectedSymbols.length > 0,
`readelf listed no symbols for ${path}`,
);
const expectedRelocations = [];
for (const [, name, count, body] of stdout.matchAll(
/^Relocation section '([^']+)' at offset 0x[0-9a-f]+ contains (\d+) entr(?:y|ies):\n((?:[^\n]+\n?)*)/gm,
)) {
const section = expectedSections.find((section) => section.name === name);
assert.notEqual(
section,
undefined,
`readelf relocation section absent: ${name}`,
);
const rows = [...body.matchAll(/^\s*([0-9a-f]+)\s+([0-9a-f]+)\s+\S+/gm)];
assert.equal(
rows.length,
Number(count),
`readelf relocation count: ${name}`,
);
for (const [entryIndex, [, offset, info]] of rows.entries()) {
// ELF32 r_info uses the low byte for type and the upper 24 bits for
// the symbol index. readelf has already decoded the file's byte order.
const decodedInfo = BigInt(`0x${info}`);
expectedRelocations.push({
section_index: section.index,
entry_index: entryIndex,
reported_offset: BigInt(`0x${offset}`),
type: Number(decodedInfo & 0xffn),
symbol_table_index: section.link,
symbol_index: Number(decodedInfo >> 8n),
});
}
}
assert.ok(
expectedRelocations.length > 0,
`readelf listed no relocations for ${path}`,
);
const bytes = await readFile(path);
for (const mode of ["cli", "mcp"]) {
const value = await inspect(mode, path);
assert.deepEqual(value.architecture, {
machine,
bits: 32,
byte_order: byteOrder,
});
assert.equal(
value.image_type,
kind === "executable" ? "ET_DYN" : "ET_REL",
);
for (const section of expectedSections) {
const reported = value.sections[section.index];
assert.equal(reported.name.display, section.name);
assert.equal(BigInt(reported.address), section.address);
assert.equal(BigInt(reported.offset), section.offset);
assert.equal(BigInt(reported.size), section.size);
}
assert.equal(value.sections.length, expectedSections.length + 1);
assert.deepEqual(
value.symbols.map(({ table_index, entry_index, value, size }) => ({
table_index,
entry_index,
value: BigInt(value),
size: BigInt(size),
})),
expectedSymbols,
`${mode} symbol values differ from readelf for ${path}`,
);
assert.deepEqual(
value.relocations.map(
({
section_index,
entry_index,
reported_offset,
type,
symbol_table_index,
symbol_index,
}) => ({
section_index,
entry_index,
reported_offset: BigInt(reported_offset),
type,
symbol_table_index,
symbol_index,
}),
),
expectedRelocations,
`${mode} relocations differ from readelf for ${path}`,
);
if (kind === "executable") {
assert.ok(value.segments.length > 0);
assert.equal(value.linkage.interpreters.length, 1);
assert.ok(
value.linkage.needed_libraries.some(
(library) => library.display === "libc.so.6",
),
);
}
assert.deepEqual(await readFile(path), bytes);
cases++;
}
}
const protectedReport = reports.get("protected");
// Ordinary data markers are not a request to discover/decompress a kernel config.
const markerBytes = Buffer.from(await readFile(join(root.path, "protected")));
+2 -2
View File
@@ -8,13 +8,13 @@
"url": "https://github.com/morluto/rea",
"source": "github"
},
"version": "6.7.0",
"version": "6.8.0",
"packages": [
{
"registryType": "npm",
"registryBaseUrl": "https://registry.npmjs.org",
"identifier": "rea-agents",
"version": "6.7.0",
"version": "6.8.0",
"runtimeHint": "npx",
"transport": {
"type": "stdio"
@@ -8,8 +8,13 @@ import {
} from "../domain/analysisErrorCore.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import type { Evidence } from "../domain/evidence.js";
import type { BrowserScenario } from "../domain/browserScenario.js";
import { err, type Result } from "../domain/result.js";
import {
browserScenarioSecretAvailability,
type BrowserScenario,
} from "../domain/browserScenario.js";
import { BROWSER_SCENARIO_START_STEP_ID } from "../domain/browserScenarioValues.js";
import type { AnalysisInputError } from "../domain/analysisErrorCore.js";
import { err, ok, type Result } from "../domain/result.js";
import { browserScenarioCaptureSchema } from "../domain/browserScenarioCapture.js";
import {
completeProviderEvidence,
@@ -82,3 +87,58 @@ export const captureBrowserScenario = async (
options.signal,
);
};
/** What a dry run checked, and the explicit stages it stopped before. */
export interface BrowserScenarioPlan {
readonly operation: typeof OPERATION;
readonly dry_run: true;
readonly browser_mode: BrowserScenario["browser"]["mode"];
readonly start_origin: string;
/** Steps in capture order, including the initial navigation step. */
readonly steps: readonly {
readonly step_id: string;
readonly action: string;
}[];
readonly secrets: readonly {
readonly secret_id: string;
readonly environment_variable: string;
readonly environment_value: "present";
}[];
readonly not_checked: readonly string[];
}
const NOT_CHECKED = [
"browser executable, CDP endpoint and target discovery",
"browser launch or attachment",
"navigation, network reachability and page state",
"locator resolution, action outcomes and capture limits",
] as const;
/**
* Apply the execution rules that need no browser to an already parsed
* scenario, then stop before any provider is created. Secret values are
* never read.
*/
export const planBrowserScenario = (
scenario: BrowserScenario,
environment: Readonly<Record<string, string | undefined>>,
): Result<BrowserScenarioPlan, AnalysisInputError> => {
const available = browserScenarioSecretAvailability(scenario, environment);
if (!available.ok) return available;
return ok({
operation: OPERATION,
dry_run: true,
browser_mode: scenario.browser.mode,
start_origin: new URL(scenario.start_url.url).origin,
steps: [
{ step_id: BROWSER_SCENARIO_START_STEP_ID, action: "goto_start" },
...scenario.actions.map(({ step_id, action }) => ({ step_id, action })),
],
secrets: scenario.secrets.map(({ secret_id, environment_variable }) => ({
secret_id,
environment_variable,
environment_value: "present" as const,
})),
not_checked: NOT_CHECKED,
});
};
@@ -378,6 +378,31 @@ it.each([
if (!result.ok) expect(result.error._tag).toBe("AnalysisInputError");
});
it("reports an ordinal past the capture's records as an out-of-range selection", async () => {
const report = fixture();
const result = await new WebNetworkCaptureService({
inspect: () => Promise.resolve(ok(report)),
}).inspect({
capture_path: "/capture.har",
format: "har",
record_ordinals: [0, 5],
});
if (result.ok) throw new Error("expected an out-of-range ordinal");
expect(projectAnalysisError(result.error)).toMatchObject({
code: "invalid_request",
details: {
issues: [
{
path: ["record_ordinals", 1],
reason: "out_of_range",
minimum: 0,
maximum: report.total_records - 1,
},
],
},
});
});
it("rejects a port changing the artifact identity or producer ordinal sequence", async () => {
const report = fixture();
report.artifact.path = "/different.har";
+15 -6
View File
@@ -155,16 +155,25 @@ export class WebNetworkCaptureService {
const selected =
input.record_ordinals ?? value.records.map(({ ordinal }) => ordinal);
const records = [];
for (const ordinal of selected) {
for (const [index, ordinal] of selected.entries()) {
const record = safeValue.records[ordinal];
// A selection past the capture's records is a caller range fault.
if (record === undefined)
return err(
new AnalysisInputError(OPERATION, undefined, [
{
path: ["record_ordinals"],
reason: "invalid_format",
message: `Producer ordinal ${ordinal} is outside the retained capture (${value.total_records} records).`,
},
value.total_records === 0
? {
path: ["record_ordinals", index],
reason: "invalid_value",
message: "The selected capture contains no records.",
}
: {
path: ["record_ordinals", index],
reason: "out_of_range",
minimum: 0,
maximum: value.total_records - 1,
message: `Producer ordinal ${ordinal} is outside the retained capture (${value.total_records} records).`,
},
]),
);
records.push({
@@ -22,6 +22,8 @@ const report: AppleIps = {
bundle_id: null,
incident_id: null,
name: null,
build_version: null,
platform: null,
unlisted_keys: [],
},
process: {
@@ -29,6 +31,7 @@ const report: AppleIps = {
path: null,
pid: null,
parent: null,
parent_pid: null,
cpu_type: null,
translated: null,
os_version: null,
@@ -200,19 +203,23 @@ it.each(["threads", "images"] as const)(
);
it.each([
[43, false],
[44, true],
[131, false],
[132, true],
] as const)(
"checks repeated integer text against %s UTF-16 bytes",
async (maximum, accepted) => {
const value = { decimal: "12345", hex: "0x3039" };
const repeated: AppleIps = {
...report,
process: { ...report.process, pid: value },
metadata: { ...report.metadata, platform: value },
process: { ...report.process, pid: value, parent_pid: value },
termination: {
namespace: null,
code: value,
indicator: null,
flags: value,
by_pid: value,
by_proc: null,
reasons: null,
unlisted_keys: [],
},
@@ -56,6 +56,28 @@ it("records the decoded layout once in its Evidence", async () => {
expect(evidence.raw_result).toBeNull();
});
it.each([
[{ machine: "EM_X86_64", bits: 64, byte_order: "little" }, "x86_64"],
[{ machine: "EM_386", bits: 32, byte_order: "little" }, "x86"],
[{ machine: "EM_ARM", bits: 32, byte_order: "little" }, "arm"],
[{ machine: "EM_MIPS", bits: 32, byte_order: "big" }, "mips"],
] as const)(
"names the reported %j machine in its Evidence subject",
async (architecture, expected) => {
const layout = { ...binaryLayoutFixture(), architecture };
const service = new BinaryLayoutService({
inspectAndClose(input, options) {
return this.inspect(input, options);
},
identity: BINARY_LAYOUT_TEST_PROVIDER,
inspect: () => Promise.resolve(ok(layout)),
});
const result = await service.inspect({ path: layout.artifact.path });
if (!result.ok) throw result.error;
expect(parseEvidence(result.value).subject?.architecture).toBe(expected);
},
);
it("preserves a provider's actionable unsupported reason", async () => {
const failure = new AnalysisCapabilityUnavailableError(
"fixture-layout",
@@ -15,6 +15,12 @@ import { err, ok, type Result } from "../../domain/result.js";
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
const OPERATION = "inspect_binary_layout";
const EVIDENCE_ARCHITECTURES = {
EM_X86_64: "x86_64",
EM_386: "x86",
EM_ARM: "arm",
EM_MIPS: "mips",
} as const;
/** Shared CLI/MCP validation and Evidence composition for offline layout inspection. */
export class BinaryLayoutService {
@@ -87,7 +93,7 @@ export class BinaryLayoutService {
{
path: value.artifact.path,
format: "elf",
architecture: "x86_64",
architecture: EVIDENCE_ARCHITECTURES[value.architecture.machine],
sha256: value.artifact.sha256,
},
this.provider.identity,
@@ -10,6 +10,12 @@ import {
referencedCreateRows,
shelvedRows,
} from "../../../tests/fixtures/environmentStateDerivedValues.js";
import {
LISTED_LABEL_ROWS,
READ_BACK_TAG_ROWS,
RESTAMPED_ROWS,
STALE_SLUG_ROWS,
} from "../../../tests/fixtures/environmentStateWriteViews.js";
import { servedFrom } from "../../../tests/fixtures/environmentStateServing.js";
import {
bookClock,
@@ -180,3 +186,72 @@ describe("served objects that repeat a named entity, and sorted arrays", () => {
).toEqual(["fog", "ice", "wind"]);
});
});
describe("served writes whose answers show the entity otherwise than reads do", () => {
it("answers a create as the application answered it and holds the fields its read-back showed", () => {
const tags = servedFrom(READ_BACK_TAG_ROWS);
const created = objectOf(
tags.answer("post_tags", { body: { name: "Saltmarsh Gate" } }).body,
);
expect(Object.keys(created).sort()).toEqual(
["created", "id", "name", "slug"].sort(),
);
expect(created).toMatchObject({
name: "Saltmarsh Gate",
slug: "saltmarsh-gate",
created: firstServedClock(READ_BACK_TAG_ROWS).replace("Z", ".000000000Z"),
});
expect(
tags.answer("get_tags_by_id", { id: String(created["id"]) }).body,
).toMatchObject({
name: "Saltmarsh Gate",
document_count: 0,
created: firstServedClock(READ_BACK_TAG_ROWS),
});
});
it("lists a created label with the count its list read showed", () => {
const labels = servedFrom(LISTED_LABEL_ROWS);
labels.answer("post_labels", { body: { title: "Slipway" } });
expect((labels.answer("get_labels").body as JsonValue[]).at(-1)).toEqual({
id: 3,
title: "Slipway",
count: { posts: 0 },
});
});
it("slugs a renamed tag anew whatever slug the call sends back", () => {
const tags = servedFrom(STALE_SLUG_ROWS);
const renamed = objectOf(
tags.answer("put_tags_by_id", {
id: "2",
body: {
id: 2,
slug: "tide-tables",
name: "Neap Tide Tables",
document_count: 0,
created: "2026-10-01T10:00:00Z",
},
}).body,
);
expect(renamed).toMatchObject({
name: "Neap Tide Tables",
slug: "neap-tide-tables",
});
});
it("stamps a post's modification time anew rather than storing the one the call sends back", () => {
const posts = servedFrom(RESTAMPED_ROWS);
const edited = objectOf(
posts.answer("put_posts_by_id", {
id: "1",
body: { id: 1, title: "Winter rota v4", updated_at: bookClock(4) },
}).body,
);
expect(edited["title"]).toBe("Winter rota v4");
expect(edited["updated_at"]).not.toBe(bookClock(4));
expect(Date.parse(String(edited["updated_at"]))).toBeGreaterThan(
Date.parse(bookClock(4)),
);
});
});
@@ -29,6 +29,7 @@ import {
type StateWritePlan,
type WriteCall,
} from "../../domain/analysisView/environmentStateWriteRules.js";
import { withRead } from "../../domain/analysisView/environmentStateWriteObserve.js";
import {
formatClock,
generatedIntegerDigits,
@@ -139,17 +140,30 @@ const callBody = ({
: err(unloadable(tool, `Member ${member}`));
};
/** The entity a recorded observation shows: the values a create keeps and an update's constants. */
/** The object a recorded response shows at a pointer. */
const recordedObject = (
serving: StateServing,
at: { readonly ordinal: number; readonly pointer: string },
): JsonObject | undefined => {
const exchange = serving.source.exchanges.get(at.ordinal);
const body = exchange === undefined ? undefined : recordedJson(exchange);
const object = body === undefined ? undefined : valueAt(body, at.pointer);
return isObject(object) ? object : undefined;
};
/** The entity a recorded observation shows, with a create's read-back over it: the values a create keeps and an update's constants. */
export const observedEntity = (
serving: StateServing,
plan: StateWritePlan,
): JsonObject | undefined => {
if (plan.observation === null) return undefined;
const exchange = serving.source.exchanges.get(plan.observation.ordinal);
const body = exchange === undefined ? undefined : recordedJson(exchange);
const object =
body === undefined ? undefined : valueAt(body, plan.observation.pointer);
return isObject(object) ? object : undefined;
const object = recordedObject(serving, plan.observation);
if (object === undefined || plan.observation.read === undefined)
return object;
const read = recordedObject(serving, plan.observation.read);
return read === undefined
? undefined
: withRead(object, read, plan.observation.read.shape);
};
/** One past the largest integer these entities hold in the field, read as the exemplar spells it, or undefined when none holds one. */
@@ -347,7 +361,15 @@ export const writeAnswer = (
: writtenResponse(
plan.response,
recorded,
rendered,
{
...rendered,
clocks:
plan.observation?.read?.shape === "item"
? plan.rules.flatMap((rule) =>
rule.rule === "clock" ? [rule.field] : [],
)
: [],
},
optionalFields(plan),
);
if (!body.ok) return err(refused(tool, body.error));
+11 -28
View File
@@ -2,7 +2,10 @@ import { AnalysisInputError } from "../domain/analysisErrorCore.js";
import { err, ok, type Result } from "../domain/result.js";
import { createHash } from "node:crypto";
import type { BrowserScenario } from "../domain/browserScenario.js";
import {
browserScenarioSecretAvailability,
type BrowserScenario,
} from "../domain/browserScenario.js";
import type {
BrowserScenarioUrl,
BrowserScenarioValue,
@@ -287,34 +290,14 @@ export class BrowserScenarioSecrets {
scenario: BrowserScenario,
environment: Readonly<Record<string, string | undefined>>,
): Result<BrowserScenarioSecrets, AnalysisInputError> {
const available = browserScenarioSecretAvailability(scenario, environment);
if (!available.ok) return err(available.error);
const values = new Map<string, string>();
for (const [index, declaration] of scenario.secrets.entries()) {
const value = environment[declaration.environment_variable];
if (
!Object.hasOwn(environment, declaration.environment_variable) ||
typeof value !== "string"
)
return err(
new AnalysisInputError(
"capture_browser_scenario",
{
userMessage: `Set environment variable ${declaration.environment_variable} for browser scenario secret ${declaration.secret_id} in the environment that starts REA, then retry.`,
},
[
{
path: ["secrets", index, "environment_variable"],
reason: "missing_argument",
message: `Environment variable ${declaration.environment_variable} is not set for declared secret ${declaration.secret_id}.`,
expected: {
secret_id: declaration.secret_id,
environment_variable: declaration.environment_variable,
},
},
],
),
);
values.set(declaration.secret_id, value);
}
for (const declaration of scenario.secrets)
values.set(
declaration.secret_id,
environment[declaration.environment_variable]!,
);
return ok(new BrowserScenarioSecrets(values));
}
@@ -73,6 +73,25 @@ it("retains allowed whitespace, visible text, and valid UTF-8 obs-text", async (
expect(result.examined_bytes).toBe(5 + Buffer.byteLength(text));
});
it("excludes trailing whitespace from trailer field values", async () => {
const text =
"grpc-status: 0 \r\ngrpc-message: alpha\tbeta \t\r\nx-empty: \t\r\n";
const result = await decodeProtobufBody(
trailerFrame(text),
"grpc-web",
source,
same,
new ProtobufBudget(),
);
expect(result.status).toBe("complete");
expect(result.frames[0]?.trailers).toEqual([
{ name: "grpc-status", value: "0" },
{ name: "grpc-message", value: "alpha\tbeta" },
{ name: "x-empty", value: "" },
]);
expect(result.examined_bytes).toBe(5 + Buffer.byteLength(text));
});
it.each(["grpc-web", "grpc-web-text"] as const)(
"keeps valid trailer fields around an opaque non-UTF-8 value in %s framing",
async (framing) => {
@@ -84,9 +84,13 @@ export const parseGrpcWebTrailers = (
"gRPC-web trailer field value contains a forbidden control character",
});
}
// RFC 9110 section 5.5: a field value excludes surrounding whitespace.
let valueEnd = lineEnd;
while (valueEnd > valueStart && [9, 32].includes(bytes[valueEnd - 1]!))
valueEnd -= 1;
let value: string;
try {
value = decoder.decode(bytes.subarray(valueStart, lineEnd));
value = decoder.decode(bytes.subarray(valueStart, valueEnd));
} catch (cause) {
if (!(cause instanceof TypeError)) throw cause;
decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
+16 -2
View File
@@ -43,13 +43,27 @@ const isObjectArray = (value: unknown): value is Record<string, unknown>[] =>
) &&
value.some((item) => Object.keys(item).length > 0);
const isWordCharacter = (character: string | undefined): boolean =>
character !== undefined && /[\p{L}\p{N}]/u.test(character);
/** Underscore runs between word characters cannot open or close emphasis. */
const escapeInlineSyntax = (
match: string,
offset: number,
text: string,
): string =>
match.startsWith("_") &&
isWordCharacter(text[offset - 1]) &&
isWordCharacter(text[offset + match.length])
? match
: match.replace(/./g, "\\$&");
const escapeTableCell = (value: string): string =>
value
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/\\/g, "\\\\")
.replace(/\|/g, "\\|")
.replace(/[\\|`*[\]~]|_+/g, escapeInlineSyntax)
.replace(/\r\n|\r|\n/g, "<br>");
const renderTable = (
+24 -4
View File
@@ -1,7 +1,10 @@
import { Cli } from "incur";
import { z } from "zod";
import { captureBrowserScenario } from "./application/BrowserScenarioCaptureService.js";
import {
captureBrowserScenario,
planBrowserScenario,
} from "./application/BrowserScenarioCaptureService.js";
import { withCommandCancellation } from "./cli/commandCancellation.js";
import { CLI_COMMANDS } from "./cliCommandNames.js";
import { parseCliJsonInput, resolveCliJsonPaths } from "./cliJsonInput.js";
@@ -38,9 +41,20 @@ export const registerBrowserScenarioCommands = (
.string()
.describe("Inline browser scenario JSON or JSON file path"),
}),
run: ({ args }) =>
options: z.object({
dryRun: z
.boolean()
.default(false)
.describe(
"Validate the scenario and its secret environment variables, then stop before discovering, launching or attaching to a browser",
),
}),
run: ({ args, options: { dryRun } }) =>
withCommandCancellation((signal) =>
logCliCommand(logger, CLI_COMMANDS.captureBrowserScenario, async () => {
const failure = dryRun
? "Browser scenario validation failed"
: "Browser scenario capture failed";
const input = await parseCliJsonInput(
args.inputJson,
OPERATION,
@@ -58,7 +72,12 @@ export const registerBrowserScenarioCommands = (
input.value,
{ cause: scenario.error },
),
failure,
);
if (dryRun) {
const plan = planBrowserScenario(scenario.data, environment);
return plan.ok ? plan.value : cliError(plan.error, failure);
}
// Load Playwright only for this command, not during CLI startup.
const provider =
options.createProvider === undefined
@@ -74,7 +93,7 @@ export const registerBrowserScenarioCommands = (
result = await captureBrowserScenario(provider, scenario.data, {
signal,
});
return result.ok ? result.value : cliError(result.error);
return result.ok ? result.value : cliError(result.error, failure);
} finally {
try {
await provider?.close?.();
@@ -96,7 +115,8 @@ export const registerBrowserScenarioCommands = (
const cliError = (
error: Parameters<typeof projectAnalysisError>[0],
label: string,
): JsonValue => ({
error: "Browser scenario capture failed",
error: label,
...projectAnalysisError(error),
});
+3 -1
View File
@@ -13,10 +13,12 @@ export interface GhidraLanguageOverride {
// Ghidra language IDs are four colon-separated fields; variants may contain
// spaces ("x86:LE:16:Real Mode") but never leading or trailing whitespace.
// Stock .ldefs also use "/" in processors ("M16C/60") and LEBE for ARM BE8
// (big-endian data, little-endian instructions).
export const ghidraLanguageIdSchema = z
.string()
.regex(
/^[A-Za-z0-9_.+-]+:(?:LE|BE):[1-9]\d*:[A-Za-z0-9_.+-](?:[A-Za-z0-9_.+ -]*[A-Za-z0-9_.+-])?$/u,
/^[A-Za-z0-9_.+-]+(?:\/[A-Za-z0-9_.+-]+)*:(?:LE|BE|LEBE):[1-9]\d*:[A-Za-z0-9_.+-](?:[A-Za-z0-9_.+ -]*[A-Za-z0-9_.+-])?$/u,
"must be a Ghidra language ID such as x86:LE:32:default",
);
@@ -250,7 +250,7 @@ export const BINARY_DIAGNOSTICS_TOOL_CONTRACTS = [
...toolContractMetadata("inspect_binary_layout"),
kind: "native-provider",
description:
"Inspect an explicit local binary without launching it as a host process. Returns file-backed section/segment ranges, linked addresses, original symbol/REL/RELA table identities, complete encoded RELR tables and upstream-derived offsets with unknown per-word locations/addends, raw name bytes, dependency/interpreter names and static mitigation inferences inline with artifact SHA-256 Evidence. Initial profile: ELF64 x86-64 little-endian EXEC/DYN/REL on Linux x64 via caller-supplied Python with unchanged pwntools 4.15.0, pyelftools 0.33 and Unicorn 2.1.2. Runtime addresses/library paths, overall relocation inventory completeness and complete derived GOT/PLT coverage remain unknown. Zero executable entry values mean absence; relocatable entries are not applicable. Upstream can emulate PLT instructions with Unicorn for derived static maps. Uses bounded complete output and owned temporary files/processes; malformed, unsupported or oversized objects return no partial success. Select detail summary to retain the complete layout Evidence in this session and receive only its summary view, then inspect sections, symbols, mitigations or linkage with inspect_analysis_view. pwntools_timeout_ms is each owned pwntools command's deadline (omitted means 30000 ms, range 1..2147483647, next call). cpuSeconds 30 and the output, diagnostic, and address-space byte allowances have no runtime override.",
"Inspect an explicit local binary without launching it as a host process. Returns file-backed section/segment ranges, linked addresses, original symbol/REL/RELA table identities, complete encoded RELR tables and upstream-derived offsets with unknown per-word locations/addends, raw name bytes, dependency/interpreter names and static mitigation inferences inline with artifact SHA-256 Evidence. Profile: EXEC/DYN/REL ELF64 x86-64, ELF32 i386, ELF32 little-endian ARM and ELF32 little- or big-endian MIPS on Linux x64 via caller-supplied Python with unchanged pwntools 4.15.0, pyelftools 0.33 and Unicorn 2.1.2. Runtime addresses/library paths, overall relocation inventory completeness and complete derived GOT/PLT coverage remain unknown. Zero executable entry values mean absence; relocatable entries are not applicable. Upstream can emulate PLT instructions with Unicorn for derived static maps. Uses bounded complete output and owned temporary files/processes; malformed, unsupported or oversized objects return no partial success. Select detail summary to retain the complete layout Evidence in this session and receive only its summary view, then inspect sections, symbols, mitigations or linkage with inspect_analysis_view. pwntools_timeout_ms is each owned pwntools command's deadline (omitted means 30000 ms, range 1..2147483647, next call). cpuSeconds 30 and the output, diagnostic, and address-space byte allowances have no runtime override.",
inputSchema: inspectBinaryLayoutRequestSchema,
outputSchema: analysisDetailOutputSchemaOf(
binaryLayoutSchema,
@@ -350,7 +350,20 @@ const writePlanSchema = z.strictObject({
.nullable()
.describe("The first successful recorded call the plan was built from."),
observation: z
.strictObject({ ordinal: ordinalSchema, pointer: z.string() })
.strictObject({
ordinal: ordinalSchema,
pointer: z.string(),
read: z
.strictObject({
ordinal: ordinalSchema,
pointer: z.string(),
shape: z.enum(["item", "list"]),
})
.exactOptional()
.describe(
"A create's later read of the entity: an item read lends fields the response lacks and its clock spellings, a list read only adds fields the response lacks.",
),
})
.nullable()
.describe(
"The record whose response showed the written entity or document, and a JSON pointer to it.",
@@ -33,6 +33,7 @@ import {
echoed,
integerOf,
readClock,
respellClock,
isUuid,
replaceDelimited,
slugOf,
@@ -488,7 +489,12 @@ export const optionalFields = (
export const writtenResponse = (
response: WriteResponse,
recorded: JsonValue | undefined,
written: { readonly fields: JsonObject | undefined; readonly id: string },
written: {
readonly fields: JsonObject | undefined;
readonly id: string;
/** Generated clocks keep the response's spelling independently of the stored entity's. */
readonly clocks?: readonly string[];
},
optional: readonly string[] = [],
): Applied<JsonValue | undefined> => {
if (response.shape === "empty") return ok(undefined);
@@ -525,7 +531,17 @@ export const writtenResponse = (
recorded,
pointer,
Object.fromEntries(
fields.map((field) => [field, written.fields![field]!]),
fields.map((field) => {
const value = written.fields![field]!;
const clock =
written.clocks?.includes(field) === true
? readClock(shown[field])
: undefined;
return [
field,
clock === undefined ? value : respellClock(value, clock.format),
];
}),
),
),
);
@@ -104,7 +104,35 @@ export interface UpdateCall {
readonly kept: readonly string[];
}
/** Whether a rule a later call showed gives an earlier call a value other than the one it showed for a field it did not send. */
/** Ambiguous saves may echo an already-derived slug or hold a clock in its own second. Keep the demonstrated derivation; call evidence still has to reproduce it. */
export const reconciledUpdateRule = (
held: FieldRule,
later: FieldRule,
kept: ReadonlySet<string>,
): FieldRule | undefined => {
const alike = alikeRules(held, later, kept);
if (alike !== undefined) return alike;
for (const [derived, ambiguous] of [
[held, later],
[later, held],
] as const) {
if (
derived.rule === "slug" &&
ambiguous.rule === "echo" &&
ambiguous.sorted !== true
)
return derived;
if (
derived.rule === "clock" &&
ambiguous.rule === "held-clock" &&
same(derived.format, ambiguous.format)
)
return derived;
}
return undefined;
};
/** Whether a later rule reproduces earlier calls, including echoes promoted to derivations. */
export const contradictsEarlierCalls = (
draft: {
readonly plan: { readonly rules: readonly FieldRule[] };
@@ -112,11 +140,12 @@ export const contradictsEarlierCalls = (
},
rules: readonly FieldRule[],
): boolean => {
const ruled = new Set(draft.plan.rules.map((rule) => rule.field));
const ruled = new Map(draft.plan.rules.map((rule) => [rule.field, rule]));
return rules.some((rule) => {
if (
!(unsentChange(rule) || regenerates(rule) || rule.rule === "clock") ||
ruled.has(rule.field)
(ruled.has(rule.field) &&
!(rule.rule === "slug" && ruled.get(rule.field)?.rule === "echo"))
)
return false;
// Compute a structured rule's digest once, rather than once per earlier call.
@@ -148,9 +177,12 @@ export const contradictsEarlierCalls = (
);
}
const from = rule.rule === "slug" ? sent.get(rule.from) : undefined;
// A slug rule runs only when its source is sent; otherwise serving keeps the field.
return (
typeof from === "string" &&
!sameObserved(observation, observedValue(slugOf(from)))
rule.rule === "slug" &&
(typeof from === "string"
? !sameObserved(observation, observedValue(slugOf(from)))
: !kept.includes(rule.field))
);
});
});
@@ -205,7 +237,10 @@ export const conflictsWithPlan = (
(identity !== null && fixed.has(identity)) ||
explained.rules.some((rule) => {
const held = rules.get(rule.field);
return held !== undefined && alikeRules(held, rule, kept) === undefined;
return (
held !== undefined &&
reconciledUpdateRule(held, rule, kept) === undefined
);
})
);
};
@@ -9,6 +9,7 @@ import type { FieldRule, WriteCall } from "./environmentStateWriteRules.js";
import {
integerOf,
readClock,
slugOf,
sortedStrings,
} from "./environmentStateWriteValues.js";
import { freshUuid, renewedRule } from "./environmentStateWriteGenerated.js";
@@ -139,7 +140,35 @@ export const sameSecondClocks = (evidence: UpdateEvidence): string[] =>
inCallSecond(evidence.after[field], evidence.startMs),
);
/** How an update stored a sent member: stamped anew as the one held clock it sent back other than a creation clock, echoed, renewed as a fresh UUID, or as a constant. */
/** Another sent member stored as sent whose slug the application stored in place of what this member sent. */
const sentSlugSource = (
evidence: UpdateEvidence,
member: string,
): string | undefined => {
const stored = evidence.after[member];
if (typeof stored !== "string") return undefined;
return Object.entries(evidence.call.body).find(
([other, value]) =>
other !== member &&
typeof value === "string" &&
same(evidence.after[other], value) &&
slugOf(value) === stored,
)?.[0];
};
/** Whether a call sent back the clock the entity held, as a whole-object write does, and the application stamped it anew near the call. */
const restampedClock = (
evidence: UpdateEvidence,
member: string,
sent: JsonValue,
): boolean =>
!CREATION_CLOCK.test(member) &&
readClock(sent) !== undefined &&
same(evidence.before[member], sent) &&
readClock(evidence.after[member]) !== undefined &&
isClockNear(evidence.after[member]!, evidence.startMs);
/** How an update stored a sent member: stamped anew as the one held clock it sent back other than a creation clock, echoed, stamped anew or slugged from another member in place of what it sent, renewed as a fresh UUID, or as a constant. */
export const sentRule = (
evidence: UpdateEvidence,
member: string,
@@ -156,6 +185,14 @@ export const sentRule = (
return { field: member, rule: "echo" };
if (same(evidence.after[member], sortedStrings(sent)))
return { field: member, rule: "echo", sorted: true };
if (restampedClock(evidence, member, sent))
return {
field: member,
rule: "clock",
format: readClock(evidence.after[member])!.format,
};
const from = sentSlugSource(evidence, member);
if (from !== undefined) return { field: member, rule: "slug", from };
return freshUuid(evidence, member)
? { field: member, rule: "unique" }
: { field: member, rule: "constant", value: evidence.after[member]! };
@@ -61,6 +61,8 @@ import {
import {
laterObservation,
newEntityIn,
respelledInstant,
withRead,
type Observed,
} from "./environmentStateWriteObserve.js";
import {
@@ -97,6 +99,57 @@ const respondedId = (
return members.length === 1 ? scalarText(members[0]) : undefined;
};
/**
* A create's answer with the first read of its entity before the next write over it, when that read shows fields the
* answer lacks or spells its clocks to another precision: the state holds what reads show. An item read that agrees
* with the answer otherwise lends it its fields and then observes rather than checks; one that disagrees still checks
* the create. A list read, which may show fewer keys of an object, only adds fields the answer lacks and still checks.
*/
const withReadBack = (
context: EffectContext,
step: WriteStep,
target: {
readonly type: string;
readonly collection: string;
readonly idField: string;
},
own: Observed,
): Observed => {
const later = laterObservation(searchFrom(context, step), {
type: target.type,
collection: target.collection,
idField: target.idField,
id: own.id,
held: () => false,
isWrite: context.isWrite,
});
if (later === undefined) return own;
const view = context.viewOf(later.ordinal);
if (
view?.shape === "item" &&
(view.fields.some(
(field) =>
Object.hasOwn(own.object, field) && !Object.hasOwn(later.object, field),
) ||
Object.entries(later.object).some(
([field, value]) =>
Object.hasOwn(own.object, field) &&
!same(own.object[field], value) &&
!respelledInstant(own.object[field], value),
))
)
return own;
const shape = view?.shape === "item" ? "item" : "list";
const object = withRead(own.object, later.object, shape);
if (same(object, own.object)) return own;
if (shape === "item") context.observed(later.ordinal);
return {
...own,
object,
read: { ordinal: later.ordinal, pointer: later.pointer, shape },
};
};
/**
* The written entity a create's response or a later read shows. A linked create's read must name an
* id its response returned, as the read that linked it did, envelopes included.
@@ -116,7 +169,7 @@ const createdObservation = (
const own = context.io.withBody(step.record.ordinal, (body) =>
newEntityIn(step.record.ordinal, body, target.idField, held),
);
if (own !== undefined) return own;
if (own !== undefined) return withReadBack(context, step, target, own);
const { id, ids } = context.io.withBody(step.record.ordinal, (body) => {
const returned = target.linked ? respondedIds(body) : [];
// A response that names the created id alone ties the later read to that id.
@@ -320,6 +373,9 @@ const buildCreate = (
observation: {
ordinal: evidence.observed.ordinal,
pointer: evidence.observed.pointer,
...(evidence.observed.read === undefined
? {}
: { read: evidence.observed.read }),
},
response: context.io.withBody(step.record.ordinal, (body) =>
responseShapeOf(
@@ -39,6 +39,7 @@ import {
type UpdateCall,
conflictsWithPlan,
refinedBody,
reconciledUpdateRule,
} from "./environmentStateWriteCalls.js";
import {
laterObservation,
@@ -58,7 +59,6 @@ import {
observationNeeds,
} from "./environmentStateWriteAddress.js";
import {
alikeRules,
keptOrders,
pathValuesOf,
siblingTrimmedClocks,
@@ -394,7 +394,7 @@ const refineUpdate = (
const shown = later.get(rule.field);
return shown === undefined
? rule
: (alikeRules(rule, shown, draft.kept) ?? rule);
: (reconciledUpdateRule(rule, shown, draft.kept) ?? rule);
}),
...evidence.explained.rules.filter((rule) => !ruled.has(rule.field)),
];
@@ -703,7 +703,11 @@ const contradictsGeneratedClock = (
if (
rule.rule !== "clock" ||
!Object.hasOwn(after, rule.field) ||
explained.unstamped.includes(rule.field)
explained.unstamped.includes(rule.field) ||
explained.rules.some(
(candidate) =>
candidate.field === rule.field && candidate.rule === "held-clock",
)
)
return false;
const previous = readClock(before[rule.field]);
@@ -15,6 +15,41 @@ import {
type StateView,
} from "./environmentStateRules.js";
import { queryValues, replayReason } from "./environmentStateVariants.js";
import { readClock } from "./environmentStateWriteValues.js";
/** A read a create's observation takes fields from: an item read's respell the response's clocks and add fields, a list read's only add fields. */
export interface ObservedRead {
readonly ordinal: number;
readonly pointer: string;
readonly shape: "item" | "list";
}
/** Whether two clocks agree at the coarser recorded fraction precision, retaining all digits through nanoseconds. */
export const respelledInstant = (
left: JsonValue | undefined,
right: JsonValue | undefined,
): boolean => {
const [a, b] = [readClock(left), readClock(right)];
if (a?.format.kind !== "iso" || b?.format.kind !== "iso") return false;
const [fine, coarse] =
a.format.fraction >= b.format.fraction ? [a, b] : [b, a];
if (coarse.format.kind !== "iso") return false;
const unit = 10 ** (9 - coarse.format.fraction);
const fraction = (clock: typeof fine) =>
(((clock.ms % 1000) + 1000) % 1000) * 1_000_000 + clock.subMillisecond;
return (
Math.floor(fine.ms / 1000) === Math.floor(coarse.ms / 1000) &&
Math.floor(fraction(fine) / unit) * unit === fraction(coarse)
);
};
/** A create's response object with a read's fields: an item read's values win, a list read only adds fields the response lacks. */
export const withRead = (
response: JsonObject,
read: JsonObject,
shape: ObservedRead["shape"],
): JsonObject =>
shape === "item" ? { ...response, ...read } : { ...read, ...response };
/** Where an observed entity object was found: the record and a JSON pointer into its response. */
export interface Observed {
@@ -22,6 +57,8 @@ export interface Observed {
readonly pointer: string;
readonly object: JsonObject;
readonly id: string;
/** A create's later read whose fields the object adds to the response's, and whose clock spellings an item read lends it. */
readonly read?: ObservedRead;
}
/** Objects a response carries where an entity may be, in precedence tiers: the root, one member, then the element of a one-element array at the root or in one member. */
@@ -253,10 +253,15 @@ export interface StateWritePlan {
readonly served: boolean;
readonly reason: WriteReason | null;
readonly exemplar_ordinal: number | null;
/** The record whose response showed the written entity or document, and where. */
/** The record whose response showed the written entity or document, and where; a create's later read whose fields the entity adds to the response's. */
readonly observation: {
readonly ordinal: number;
readonly pointer: string;
readonly read?: {
readonly ordinal: number;
readonly pointer: string;
readonly shape: "item" | "list";
};
} | null;
readonly response: WriteResponse | null;
readonly body: WriteBody | null;
@@ -0,0 +1,172 @@
import { describe, expect, it } from "vitest";
import type { JsonValue } from "../jsonValue.js";
import {
timedCaptureEvidence,
type TimedCaptureRow,
} from "../../../tests/fixtures/analysisView.js";
import {
LISTED_LABEL_ROWS,
READ_BACK_TAG_ROWS,
RESTAMPED_ROWS,
STALE_SLUG_ROWS,
clockSaveRows,
slugSaveRows,
slugOnlySaveRows,
} from "../../../tests/fixtures/environmentStateWriteViews.js";
import { environmentStateViewOf } from "../../../tests/fixtures/environmentStateWrites.js";
const planOf = (rows: readonly TimedCaptureRow[], tool: string) => {
const plan = environmentStateViewOf(timedCaptureEvidence(rows)).writes.find(
(candidate) => candidate.tool === tool,
);
if (plan === undefined) throw new Error(`No plan for ${tool}`);
return plan;
};
describe("creates whose answer shows the entity otherwise than reads do", () => {
it.each(["123456", "123457"])(
"checks read-back precision beyond milliseconds (%s)",
(fraction) => {
const rows = READ_BACK_TAG_ROWS.map(
([method, path, body = null, ...rest]): TimedCaptureRow => {
const rewrite = (value: JsonValue): JsonValue => {
if (Array.isArray(value)) return value.map(rewrite);
if (value !== null && typeof value === "object")
return Object.fromEntries(
Object.entries(value).map(([key, child]) => [
key,
key === "created" && typeof child === "string"
? child.replace(
/(?:\.\d+)?Z$/u,
method === "POST" ? ".123456000Z" : `.${fraction}Z`,
)
: child,
]),
);
return value;
};
return [method, path, rewrite(body), ...rest];
},
);
expect(planOf(rows, "post_tags").served).toBe(fraction === "123456");
},
);
it("keeps an item read's omission as contradictory evidence even when a later list confirms the added fields", () => {
const rows = READ_BACK_TAG_ROWS.map(
([method, path, body = null, ...rest]): TimedCaptureRow => [
method,
path,
body !== null &&
typeof body === "object" &&
!Array.isArray(body) &&
(method === "POST" || path === "/api/tags/1" || path === "/api/tags/2")
? { ...body, detail: "Only in the answer" }
: body,
...rest,
],
);
expect(planOf(rows, "post_tags")).toMatchObject({
served: false,
reason: "contradicted",
});
});
it("serves a create whose item read adds a count and spells the clock otherwise, taking the read's fields", () => {
expect(planOf(READ_BACK_TAG_ROWS, "post_tags")).toMatchObject({
served: true,
observation: {
ordinal: 3,
pointer: "",
read: { ordinal: 4, pointer: "", shape: "item" },
},
rules: expect.arrayContaining([
{
field: "created",
rule: "clock",
format: expect.objectContaining({ fraction: 0 }),
},
]),
});
});
it("serves a create whose list read adds a count the answer lacks", () => {
expect(planOf(LISTED_LABEL_ROWS, "post_labels")).toMatchObject({
served: true,
observation: { read: { ordinal: 2, pointer: "/2", shape: "list" } },
});
});
});
describe("whole-object updates that send back what the application derives", () => {
it.each([
[true, true],
[false, true],
[true, false],
[false, false],
] as const)(
"checks slug-only saves too (derived first: %s, changed: %s)",
(derivedFirst, changed) => {
expect(
planOf(slugOnlySaveRows(derivedFirst, changed), "patch_tags_by_id")
.served,
).toBe(!changed);
},
);
it.each([true, false])(
"reconciles slug saves in either order (derived first: %s)",
(derivedFirst) => {
expect(
planOf(slugSaveRows(derivedFirst), "put_tags_by_id"),
).toMatchObject({
served: true,
rules: expect.arrayContaining([
{ field: "slug", rule: "slug", from: "name" },
]),
});
},
);
it.each([true, false])(
"rejects a slug rule contradicted by an explicit custom slug (derived first: %s)",
(derivedFirst) => {
expect(
planOf(slugSaveRows(derivedFirst, true), "put_tags_by_id").served,
).toBe(false);
},
);
it.each([true, false])(
"reconciles held and fresh clocks in either order (derived first: %s)",
(derivedFirst) => {
expect(
planOf(clockSaveRows(derivedFirst), "put_posts_by_id"),
).toMatchObject({
served: true,
rules: expect.arrayContaining([
expect.objectContaining({ field: "updated_at", rule: "clock" }),
]),
});
},
);
it("slugs a sent slug anew from the name the call stored", () => {
expect(planOf(STALE_SLUG_ROWS, "put_tags_by_id")).toMatchObject({
served: true,
rules: expect.arrayContaining([
{ field: "slug", rule: "slug", from: "name" },
{ field: "name", rule: "echo" },
]),
body: { accepted: expect.arrayContaining(["slug", "name"]) },
});
});
it("stamps anew a modification time each call sends back as it read it", () => {
expect(planOf(RESTAMPED_ROWS, "put_posts_by_id")).toMatchObject({
served: true,
rules: expect.arrayContaining([
expect.objectContaining({ field: "updated_at", rule: "clock" }),
]),
body: { accepted: expect.arrayContaining(["updated_at"]) },
});
});
});
+9 -5
View File
@@ -324,17 +324,20 @@ const buildArtifactChangeBase = (input: ClassifyPathInput) => ({
evidence_links: [...input.evidenceLinks],
});
const classifyPresence = (input: ClassifyPathInput): ArtifactChange | null => {
const classifyPresence = (
input: ClassifyPathInput,
base: ReturnType<typeof buildArtifactChangeBase>,
): ArtifactChange | null => {
const covered = input.path === "." ? input.rootCovered : input.entriesCovered;
if (input.leftOccurrence === undefined)
return {
...buildArtifactChangeBase(input),
...base,
classification: covered ? "added" : "unknown",
dimensions: [covered ? "content" : "availability"],
};
if (input.rightOccurrence === undefined)
return {
...buildArtifactChangeBase(input),
...base,
classification: covered ? "removed" : "unknown",
dimensions: [covered ? "content" : "availability"],
};
@@ -372,11 +375,12 @@ const classifyAvailability = (
};
const classifyPath = (input: ClassifyPathInput): ArtifactChange | null => {
const presence = classifyPresence(input);
if (presence !== null) return presence;
const base = buildArtifactChangeBase(input);
// A declared/observed mismatch is known even without an opposite occurrence.
const integrity = classifyIntegrity(input, base);
if (integrity !== null) return integrity;
const presence = classifyPresence(input, base);
if (presence !== null) return presence;
const declaration = classifyPyInstallerDeclaration(input, base);
if (declaration.handled) return declaration.change;
const availability = classifyAvailability(input, base);
+38
View File
@@ -1,5 +1,8 @@
import { z } from "zod";
import { AnalysisInputError } from "./analysisErrorCore.js";
import { err, ok, type Result } from "./result.js";
import { webRuntimeCommandTimeoutSchema } from "./browserDeadline.js";
import {
browserScenarioActionSchema,
@@ -209,3 +212,38 @@ export const browserScenarioSchema = browserScenarioInputSchema.superRefine(
validateBrowserScenario,
);
export type BrowserScenario = z.infer<typeof browserScenarioSchema>;
/**
* Refuse a declared secret whose environment variable is absent. Execution and
* dry runs share this rule. It checks presence only and never reads a value.
*/
export const browserScenarioSecretAvailability = (
scenario: BrowserScenario,
environment: Readonly<Record<string, string | undefined>>,
): Result<void, AnalysisInputError> => {
for (const [index, declaration] of scenario.secrets.entries())
if (
!Object.hasOwn(environment, declaration.environment_variable) ||
typeof environment[declaration.environment_variable] !== "string"
)
return err(
new AnalysisInputError(
"capture_browser_scenario",
{
userMessage: `Set environment variable ${declaration.environment_variable} for browser scenario secret ${declaration.secret_id} in the environment that starts REA, then retry.`,
},
[
{
path: ["secrets", index, "environment_variable"],
reason: "missing_argument",
message: `Environment variable ${declaration.environment_variable} is not set for declared secret ${declaration.secret_id}.`,
expected: {
secret_id: declaration.secret_id,
environment_variable: declaration.environment_variable,
},
},
],
),
);
return ok(undefined);
};
@@ -52,6 +52,36 @@ describe("child-process options detection", () => {
]);
});
it("reads fork's silent option when stdio is not a string or array", () => {
const ir = analyzeJavaScriptSemantics(`
import { fork, spawn } from "node:child_process";
fork("piped.js", { silent: true });
fork("inherited.js", [], { silent: false });
fork("nullish-stdio.js", { silent: 1, stdio: null });
fork("explicit-stdio.js", { silent: true, stdio: "inherit" });
fork("dynamic.js", { silent: process.env.QUIET });
spawn("tool", { silent: true });
fork("boolean-stdio.js", { stdio: false, silent: true });
fork("object-stdio.js", { stdio: {}, silent: false });
fork("unresolved-stdio.js", { stdio: streams, silent: true });
spawn("tool", { stdio: false });
`);
expect(ir.childProcessSpawns.map(({ stdioMode }) => stdioMode)).toEqual([
"pipe",
"default",
"pipe",
"inherit",
"dynamic",
"default",
"pipe",
"default",
"dynamic",
"dynamic",
]);
});
it("preserves behavior with explicit args arrays", () => {
const ir = analyzeJavaScriptSemantics(`
import { spawn, execFile, fork } from "node:child_process";
@@ -14,6 +14,7 @@ import {
} from "./javascriptSemanticExpressionShapes.js";
import {
semanticStaticPropertyName,
staticJavaScriptTruthiness,
staticObjectProperty,
unwrapJavaScriptExpression,
} from "./javascriptAstValues.js";
@@ -424,15 +425,40 @@ const stdioMode = (
)
return "default";
const lookup = staticObjectProperty(expression, "stdio", context.isNullish);
if (lookup.status === "missing") return "default";
if (lookup.status === "missing")
return unsetStdioMode(expression, method, context);
if (lookup.status !== "explicit") return "dynamic";
const value = unwrapJavaScriptExpression(lookup.property.value).node;
if (context.isNullish(value)) return "default";
if (context.isNullish(value))
return unsetStdioMode(expression, method, context);
if (t.isStringLiteral(value)) return value.value;
if (t.isArrayExpression(value)) return "array";
// fork ignores any other known stdio value and falls back to `silent`.
if (method === "fork" && staticJavaScriptTruthiness(value) !== undefined)
return unsetStdioMode(expression, method, context);
return "dynamic";
};
/**
* Without a string or array `stdio`, Node's `fork` pipes the standard streams
* when `silent` is truthy and inherits them otherwise.
*/
const unsetStdioMode = (
options: t.Node,
method: JavaScriptSemanticChildProcessSpawn["method"],
context: ChildSpawnContext,
): string => {
if (method !== "fork") return "default";
const silent = staticObjectProperty(options, "silent", context.isNullish);
if (silent.status === "missing") return "default";
if (silent.status !== "explicit") return "dynamic";
const value = unwrapJavaScriptExpression(silent.property.value).node;
if (context.isNullish(value)) return "default";
const truthy = staticJavaScriptTruthiness(value);
if (truthy === undefined) return "dynamic";
return truthy ? "pipe" : "default";
};
const objectBinding = (
node: t.Expression | t.Super,
state: JavaScriptSemanticAnalysisState,
+3
View File
@@ -40,6 +40,8 @@ it("rejects extra caller arguments and admits an empty crash inventory", () => {
bundle_id: null,
incident_id: null,
name: null,
build_version: null,
platform: null,
unlisted_keys: [],
},
process: {
@@ -47,6 +49,7 @@ it("rejects extra caller arguments and admits an empty crash inventory", () => {
path: null,
pid: null,
parent: null,
parent_pid: null,
cpu_type: null,
translated: null,
os_version: null,
+11 -1
View File
@@ -122,6 +122,8 @@ const metadataSchema = z.strictObject({
bundle_id: z.string().nullable(),
incident_id: z.string().nullable(),
name: z.string().nullable(),
build_version: z.string().nullable(),
platform: recordedIntegerSchema.nullable(),
unlisted_keys: z.array(z.string()),
});
@@ -141,6 +143,9 @@ const terminationSchema = z
namespace: z.string().nullable(),
code: recordedIntegerSchema.nullable(),
indicator: z.string().nullable(),
flags: recordedIntegerSchema.nullable(),
by_pid: recordedIntegerSchema.nullable(),
by_proc: z.string().nullable(),
reasons: z.array(z.string()).nullable(),
unlisted_keys: z.array(z.string()),
})
@@ -275,6 +280,7 @@ export const appleIpsSchema = z.strictObject({
path: z.string().nullable(),
pid: recordedIntegerSchema.nullable(),
parent: z.string().nullable(),
parent_pid: recordedIntegerSchema.nullable(),
cpu_type: z.string().nullable(),
translated: z.boolean().nullable(),
os_version: osVersionSchema,
@@ -314,8 +320,12 @@ export const appleIpsIntegerTextBytes = (
/** The named report fields own which observed and derived integers are retained. */
export const appleIpsReportIntegerTextBytes = (report: AppleIps): number => {
let bytes =
appleIpsIntegerTextBytes(report.metadata.platform) +
appleIpsIntegerTextBytes(report.process.pid) +
appleIpsIntegerTextBytes(report.termination?.code ?? null);
appleIpsIntegerTextBytes(report.process.parent_pid) +
appleIpsIntegerTextBytes(report.termination?.code ?? null) +
appleIpsIntegerTextBytes(report.termination?.flags ?? null) +
appleIpsIntegerTextBytes(report.termination?.by_pid ?? null);
for (const image of report.images)
bytes +=
appleIpsIntegerTextBytes(image.base) +
+30
View File
@@ -121,6 +121,21 @@ it("retains high linked values and NOBITS without inventing file backing", () =>
expect(value.runtime_load_base).toBeNull();
});
it.each([
[{ machine: "EM_386", bits: 32, byte_order: "little" }, true],
[{ machine: "EM_ARM", bits: 32, byte_order: "little" }, true],
[{ machine: "EM_MIPS", bits: 32, byte_order: "little" }, true],
[{ machine: "EM_MIPS", bits: 32, byte_order: "big" }, true],
[{ machine: "EM_X86_64", bits: 32, byte_order: "little" }, false],
[{ machine: "EM_ARM", bits: 32, byte_order: "big" }, false],
[{ machine: "EM_386", bits: 64, byte_order: "little" }, false],
[{ machine: "EM_MIPS", bits: 64, byte_order: "big" }, false],
])("admits only profiled architecture %j: %s", (architecture, admitted) => {
expect(
binaryLayoutSchema.safeParse({ ...report(), architecture }).success,
).toBe(admitted);
});
it.each([
"file-backed-outside",
"header-outside",
@@ -276,6 +291,9 @@ it.each([
"wrong-unknown-target",
"missing-string-table",
"wrong-string-table-kind",
"elf32-symbol-entry",
"elf32-undersized-symbol-entry",
"elf64-elf32-sized-symbol-entry",
])("validates relocation symbol reference semantics: %s", (problem) => {
const value = binaryLayoutSchema.parse(report());
value.artifact.bytes = 512;
@@ -359,6 +377,17 @@ it.each([
if (problem === "owner-link-mismatch") owner.link = 0;
if (problem === "wrong-meaning")
relocation.symbol_reference_meaning = "zero-symbol-value";
// Elf32_Sym is 16 bytes and Elf64_Sym 24; the minimum follows the ELF class.
if (problem.includes("elf32")) {
if (problem.startsWith("elf32"))
value.architecture = {
machine: "EM_386",
bits: 32,
byte_order: "little",
};
table.size = "0x20";
table.entry_size = problem.includes("undersized") ? "0xc" : "0x10";
}
if (problem.includes("target")) {
value.image_type = "ET_REL";
value.entry_point.meaning = "not-applicable";
@@ -411,6 +440,7 @@ it.each([
"valid-section-target",
"undefined-target",
"inactive-target",
"elf32-symbol-entry",
].includes(problem),
);
});
+20 -6
View File
@@ -33,11 +33,23 @@ const binaryLayoutObjectSchema = z.strictObject({
bytes: index,
}),
format: z.literal("elf"),
architecture: z.strictObject({
machine: z.literal("EM_X86_64"),
bits: z.literal(64),
byte_order: z.literal("little"),
}),
architecture: z.union([
z.strictObject({
machine: z.literal("EM_X86_64"),
bits: z.literal(64),
byte_order: z.literal("little"),
}),
z.strictObject({
machine: z.enum(["EM_386", "EM_ARM"]),
bits: z.literal(32),
byte_order: z.literal("little"),
}),
z.strictObject({
machine: z.literal("EM_MIPS"),
bits: z.literal(32),
byte_order: z.enum(["little", "big"]),
}),
]),
image_type: z.enum(["ET_EXEC", "ET_DYN", "ET_REL"]),
entry_point: z.strictObject({
reported_value: unsignedHex,
@@ -384,7 +396,9 @@ const validateRelocationReferences = (
(table?.type === "SHT_SYMTAB" || table?.type === "SHT_DYNSYM");
const positiveReferenceExists =
tableExists &&
BigInt(table.entry_size) >= 24n &&
// Elf32_Sym is 16 bytes; Elf64_Sym is 24.
BigInt(table.entry_size) >=
(value.architecture.bits === 32 ? 16n : 24n) &&
BigInt(relocation.symbol_index) <
BigInt(table.size) / BigInt(table.entry_size) &&
symbols.has(
+1 -1
View File
@@ -1,7 +1,7 @@
/** Generated by scripts/generate-package-metadata.mjs; do not edit. */
export const PACKAGE_METADATA = {
name: "rea-agents",
version: "6.7.0", // x-release-please-version
version: "6.8.0", // x-release-please-version
pinnedNodeVersion: "24.22.1",
serverSdkVersion: "2.3.1",
clientSdkVersion: "2.3.1",
+9 -6
View File
@@ -33,6 +33,7 @@ import {
} from "./GhidraPowerPcProfile.js";
import { ghidraRawBinaryLanguage } from "./GhidraRawBinaryLanguage.js";
import {
GHIDRA_GAMECUBE_DOL_LANGUAGE,
ghidraDolLanguage,
ghidraDolLanguageUnsupportedReason,
ghidraDolProfileParameters,
@@ -180,14 +181,16 @@ export const resolveGhidraAnalysisProfile = async (
if (target.format === "gamecube-dol") {
// The DOL admission owns the processor family; only a BE32 PowerPC variant may replace the stock language.
const reason = ghidraDolLanguageUnsupportedReason(languageOverride);
if (reason !== null)
// The DOL is admitted; only the selected language conflicts with it.
if (reason !== null && languageOverride !== undefined)
return Promise.resolve(
err(
new AnalysisUnsupportedTargetError(
"resolve_analysis_profile",
target.path,
reason,
),
languageOverrideConflict(languageOverride, reason, {
languageId: GHIDRA_GAMECUBE_DOL_LANGUAGE.languageId,
...(languageOverride.compilerSpecId === undefined
? {}
: { compilerSpecId: languageOverride.compilerSpecId }),
}),
),
);
const language = ghidraDolLanguage(languageOverride);
+37 -5
View File
@@ -4,6 +4,7 @@ import { describe, expect, it } from "vitest";
import { parseBinaryTarget } from "../application/BinaryTargetResolver.js";
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
import { parseConfig } from "../config/parseConfig.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import { buildGamecubeDol } from "../domain/gamecubeDol.fixture.js";
import { parseDolHeader } from "../domain/gamecubeDol.js";
@@ -160,11 +161,42 @@ describe("GameCube DOL Ghidra profile", () => {
host,
);
const resolved = await ghidra.resolveAnalysisProfile(await dolTarget());
expect(resolved.ok).toBe(false);
if (!resolved.ok)
expect(resolved.error.message).toContain(
"requires a PowerPC:BE:32:* language",
);
if (resolved.ok) throw new Error("expected an override conflict");
expect(resolved.error.message).toContain(
"requires a PowerPC:BE:32:* language",
);
// The setting conflicts with the admitted DOL; the target stays supported.
expect(projectAnalysisError(resolved.error)).toMatchObject({
code: "configuration_invalid",
details: {
settings: [
expect.objectContaining({ setting: "REA_GHIDRA_LANGUAGE_ID" }),
],
},
});
});
it("names only the language when a compiler spec accompanies a conflicting language", async () => {
const ghidra = new GhidraProvider(
await installedConfig({
REA_GHIDRA_LANGUAGE_ID: "x86:LE:32:default",
REA_GHIDRA_COMPILER_SPEC_ID: "gcc",
}),
silentLogger,
{},
host,
);
const resolved = await ghidra.resolveAnalysisProfile(await dolTarget());
if (resolved.ok) throw new Error("expected an override conflict");
// DOL accepts a compiler-spec override, so only the language is blamed.
expect(projectAnalysisError(resolved.error)).toMatchObject({
code: "configuration_invalid",
details: {
settings: [
expect.objectContaining({ setting: "REA_GHIDRA_LANGUAGE_ID" }),
],
},
});
});
it("does not admit DOL through Windows Ghidra P0", async () => {
+19
View File
@@ -156,6 +156,25 @@ describe("Ghidra language override configuration", () => {
});
});
it.each([
"ARM:LEBE:32:v8LEInstruction",
"ARM:LEBE:32:v7LEInstruction",
"M16C/60:LE:16:default",
"M16C/80:LE:16:default",
])("accepts the stock Ghidra language ID %s", (languageId) => {
const parsed = parseConfig({ REA_GHIDRA_LANGUAGE_ID: languageId });
if (!parsed.ok) throw parsed.error;
expect(parsed.value.ghidraLanguageOverride).toEqual({ languageId });
});
it.each(["ARM:BELE:32:v8", "/x86:LE:32:default", "x86:LE:32:default "])(
"still refuses the malformed language ID %j",
(languageId) => {
const parsed = parseConfig({ REA_GHIDRA_LANGUAGE_ID: languageId });
expect(parsed.ok).toBe(false);
},
);
it("requires a language for a compiler spec, as analyzeHeadless does", () => {
const parsed = parseConfig({ REA_GHIDRA_COMPILER_SPEC_ID: "borlandcpp" });
if (parsed.ok) throw new Error("Expected a configuration error");
+11 -1
View File
@@ -122,7 +122,14 @@ const terminationShape = (): JsonShape => {
return {
kind: "object",
members: {
...scalarMembers("namespace", "code", "indicator"),
...scalarMembers(
"namespace",
"code",
"indicator",
"flags",
"byPid",
"byProc",
),
reasons: {
kind: "array",
items: SCALAR,
@@ -150,6 +157,7 @@ export const ipsBodyShape = (input: InspectAppleIpsInput): JsonShape => {
"procName",
"procPath",
"pid",
"parentPid",
"parentProc",
"cpuType",
"translated",
@@ -202,6 +210,8 @@ export const IPS_METADATA_SHAPE: JsonShape = {
"bundleID",
"incident_id",
"name",
"build_version",
"platform",
),
beforeUnlisted: unlistedKeys,
};
+36 -7
View File
@@ -383,11 +383,7 @@ it("keeps ReportCrash-shaped producer facts and reports complete coverage", () =
description: "(Data Abort) byte read Translation fault",
});
expect(parsed.exception?.unlisted_keys).toEqual(["rawCodes"]);
expect(parsed.termination?.unlisted_keys).toEqual([
"flags",
"byProc",
"byPid",
]);
expect(parsed.termination?.unlisted_keys).toEqual([]);
expect(parsed.unlisted_body_keys).toContain("sharedCache");
expect(parsed.unlisted_body_keys).not.toContain("osVersion");
expect(parsed.metadata.unlisted_keys).toContain("slice_uuid");
@@ -402,6 +398,37 @@ it("keeps ReportCrash-shaped producer facts and reports complete coverage", () =
});
});
it("keeps the documented terminating process, parent pid and metadata members", () => {
const parsed = report({});
expect(parsed.termination).toMatchObject({
flags: { decimal: "0", hex: "0x0" },
by_pid: { decimal: "4242", hex: "0x1092" },
by_proc: "exc handler",
});
expect(parsed.process.parent_pid).toEqual({ decimal: "100", hex: "0x64" });
expect(parsed.metadata).toMatchObject({
build_version: "",
platform: { decimal: "1", hex: "0x1" },
});
expect(parsed.metadata.unlisted_keys).not.toContain("build_version");
expect(parsed.metadata.unlisted_keys).not.toContain("platform");
expect(parsed.unlisted_body_keys).not.toContain("parentPid");
expect(parsed.coverage.status).toBe("complete");
const wrong = parse(
Buffer.from(
appleIpsReport().replace('"byPid": 4242', '"byPid": "4242"'),
"utf8",
),
);
expect(wrong.termination?.by_pid).toBeNull();
expect(wrong.coverage.type_mismatches).toContainEqual(
expect.objectContaining({
path: "body.termination.byPid",
expected: "integer",
}),
);
});
it("keeps the recorded image name and symbol offset", () => {
const parsed = report({});
expect(parsed.images.map((image) => image.name)).toEqual([
@@ -511,8 +538,10 @@ it.each(["-1", "4294967296", `1${"0".repeat(1000)}`])(
);
it("bounds rejected index text before retaining it", () => {
const bytes = withBody(
'{"faultingThread":4294967296,"threads":[],"usedImages":[]}',
// A bare metadata line: its recorded platform integer would also count toward this allowance.
const bytes = Buffer.from(
'{"bug_type":"309"}\n{"faultingThread":4294967296,"threads":[],"usedImages":[]}',
"utf8",
);
expect(() =>
parseAppleIps(bytes, artifact(bytes), {
+6
View File
@@ -86,6 +86,9 @@ const terminationOf = (
namespace: read.text(members.namespace, "body.termination.namespace"),
code: read.integer(members.code, "body.termination.code"),
indicator: read.text(members.indicator, "body.termination.indicator"),
flags: read.integer(members.flags, "body.termination.flags"),
by_pid: read.integer(members.byPid, "body.termination.byPid"),
by_proc: read.text(members.byProc, "body.termination.byProc"),
reasons:
reasonItems === null
? null
@@ -353,6 +356,8 @@ const metadataOf = (text: string, read: FieldReader): AppleIps["metadata"] => {
bundle_id: read.text(members.bundleID, "metadata.bundleID"),
incident_id: read.text(members.incident_id, "metadata.incident_id"),
name: read.text(members.name, "metadata.name"),
build_version: read.text(members.build_version, "metadata.build_version"),
platform: read.integer(members.platform, "metadata.platform"),
unlisted_keys: [...json.unlisted],
};
};
@@ -433,6 +438,7 @@ export const parseAppleIps = (
path: read.text(body.procPath, "body.procPath"),
pid: read.integer(body.pid, "body.pid"),
parent: read.text(body.parentProc, "body.parentProc"),
parent_pid: read.integer(body.parentPid, "body.parentPid"),
cpu_type: read.text(body.cpuType, "body.cpuType"),
translated: read.boolean(body.translated, "body.translated"),
os_version: osVersionOf(body.osVersion, read),
@@ -0,0 +1,201 @@
import { Cli } from "incur";
import pino from "pino";
import { describe, expect, it, vi } from "vitest";
import type { BrowserScenarioCapturePort } from "../../../src/application/BrowserScenarioCapturePort.js";
import { BrowserScenarioSecrets } from "../../../src/browser/BrowserScenarioSecrets.js";
import { registerBrowserScenarioCommands } from "../../../src/cliBrowserScenarioCommands.js";
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
import { browserScenarioSchema } from "../../../src/domain/browserScenario.js";
import { cliTest } from "../../support/cli/cliFixture.js";
const SECRET_VALUE = "dry-run-secret-value-1234";
const scenario = {
browser: { mode: "launch", executable_path: "/rea-missing/browser" },
start_url: {
url: "https://example.test/login",
query: [
{
name: "token",
value: { source: "secret", secret_id: "api_token" },
},
],
},
actions: [
{ step_id: "settle", action: "wait_for_timeout", duration_ms: 1 },
{
step_id: "account",
action: "goto",
destination: { url: "https://example.test/account" },
wait_until: "load",
},
],
secrets: [{ secret_id: "api_token", environment_variable: "REA_DRY_TOKEN" }],
};
/** One in-process CLI whose provider factory records every invocation. */
const scenarioCli = (environment: Readonly<Record<string, string>>) => {
const createProvider = vi.fn((): BrowserScenarioCapturePort => {
throw new Error("A dry run must not create a browser provider");
});
const cli = Cli.create("rea-scenario-dry-run-test", { sync: false });
registerBrowserScenarioCommands(cli, pino({ level: "silent" }), environment, {
createProvider,
});
const serve = async (input: unknown, ...flags: string[]) => {
let stdout = "";
await cli.serve(
["capture-browser-scenario", JSON.stringify(input), ...flags, "--json"],
{
env: {},
stdout: (text) => {
stdout += text;
},
exit: () => undefined,
},
);
return { stdout, json: JSON.parse(stdout) as Record<string, unknown> };
};
return { createProvider, serve };
};
describe("capture-browser-scenario --dry-run", () => {
it("returns the plan without creating a provider or reading secret values into output", async () => {
const { createProvider, serve } = scenarioCli({
REA_DRY_TOKEN: SECRET_VALUE,
});
const { stdout, json } = await serve(scenario, "--dry-run");
expect(json).toEqual({
operation: "capture_browser_scenario",
dry_run: true,
browser_mode: "launch",
start_origin: "https://example.test",
steps: [
{ step_id: "scenario_start", action: "goto_start" },
{ step_id: "settle", action: "wait_for_timeout" },
{ step_id: "account", action: "goto" },
],
secrets: [
{
secret_id: "api_token",
environment_variable: "REA_DRY_TOKEN",
environment_value: "present",
},
],
not_checked: expect.arrayContaining(["browser launch or attachment"]),
});
expect(stdout).not.toContain(SECRET_VALUE);
expect(createProvider).not.toHaveBeenCalled();
});
it("refuses a missing secret environment variable with the execution rule's error", async () => {
const { createProvider, serve } = scenarioCli({});
const { json } = await serve(scenario, "--dry-run");
const parsed = browserScenarioSchema.parse(scenario);
const execution = BrowserScenarioSecrets.resolve(parsed, {});
if (execution.ok) throw new Error("Missing secret resolved");
expect(json).toEqual({
error: "Browser scenario validation failed",
...projectAnalysisError(execution.error),
});
expect(json).toMatchObject({
code: "invalid_request",
details: {
issues: [
{
path: ["secrets", 0, "environment_variable"],
reason: "missing_argument",
},
],
},
});
// An inherited or non-string value is not an available secret.
const inherited = Object.create({ REA_DRY_TOKEN: SECRET_VALUE }) as Record<
string,
string
>;
expect(
(await scenarioCli(inherited).serve(scenario, "--dry-run")).json,
).toMatchObject({ code: "invalid_request" });
expect(createProvider).not.toHaveBeenCalled();
});
it("reports schema and semantic errors exactly as execution does", async () => {
const invalid = {
...scenario,
actions: [...scenario.actions, scenario.actions[0]],
secrets: [],
};
const dry = scenarioCli({ REA_DRY_TOKEN: SECRET_VALUE });
const executed = scenarioCli({ REA_DRY_TOKEN: SECRET_VALUE });
const { json: dryJson } = await dry.serve(invalid, "--dry-run");
const { json: executedJson } = await executed.serve(invalid);
expect(dryJson).toMatchObject({
error: "Browser scenario validation failed",
code: "invalid_request",
details: {
issues: expect.arrayContaining([
expect.objectContaining({
path: ["actions"],
message: "Action step IDs must be unique",
}),
expect.objectContaining({
path: ["secrets"],
message: "Secret reference api_token is not declared",
}),
]),
},
});
const { error: dryLabel, ...dryProjection } = dryJson;
const { error: executedLabel, ...executedProjection } = executedJson;
expect([dryLabel, executedLabel]).toEqual([
"Browser scenario validation failed",
"Browser scenario capture failed",
]);
expect(dryProjection).toEqual(executedProjection);
expect(dry.createProvider).not.toHaveBeenCalled();
expect(executed.createProvider).not.toHaveBeenCalled();
});
it("still runs execution when the flag is omitted", async () => {
const { createProvider, serve } = scenarioCli({
REA_DRY_TOKEN: SECRET_VALUE,
});
const { json } = await serve(scenario);
expect(json).not.toHaveProperty("dry_run");
expect(createProvider).toHaveBeenCalledOnce();
});
});
cliTest(
"validates a scenario through the compiled CLI without a browser",
async ({ cli }) => {
const valid = await cli.run({
arguments: [
"capture-browser-scenario",
JSON.stringify(scenario),
"--dry-run",
"--json",
],
environment: { REA_LOG_LEVEL: "silent", REA_DRY_TOKEN: SECRET_VALUE },
});
expect(valid.exitCode).toBe(0);
expect(valid.json).toMatchObject({ dry_run: true, browser_mode: "launch" });
expect(valid.stdout).not.toContain(SECRET_VALUE);
const missing = await cli.run({
arguments: [
"capture-browser-scenario",
JSON.stringify(scenario),
"--dry-run",
"--json",
],
environment: { REA_LOG_LEVEL: "silent" },
});
expect(missing.exitCode).not.toBe(0);
expect(missing.json).toMatchObject({
code: "invalid_request",
details: { issues: [{ path: ["secrets", 0, "environment_variable"] }] },
});
},
);
+11 -10
View File
@@ -4,7 +4,10 @@ import { Cli, Formatter } from "incur";
import { estimateTokenCount } from "tokenx";
import { expect, it } from "vitest";
import { parseCliOutputArguments } from "../../../src/cliOutput.js";
import {
parseCliOutputArguments,
renderCliDocument,
} from "../../../src/cliOutput.js";
import {
createCliResultOutput,
streamCliCommandResults,
@@ -80,7 +83,7 @@ it("renders only each record's own fields in heterogeneous Markdown tables", asy
"name",
"constructor",
"toString",
"__proto__",
"\\_\\_proto\\_\\_",
"enabled",
"count",
]);
@@ -346,10 +349,9 @@ it.each(["json", "jsonl", "yaml", "toon", "md"] as const)(
.split("\n")
.map((line) => JSON.parse(line)),
).toEqual(expected);
else if (format === "md") {
expect(outputs[0]).toContain(`| ${JSON.stringify(expected[0]!.rows)} |`);
expect(outputs[0]).not.toContain("[object Object]");
} else expect(outputs[0]).toBe(`${Formatter.format(expected, format)}\n`);
else if (format === "md")
expect(outputs[0]).toBe(renderCliDocument(expected, format, false));
else expect(outputs[0]).toBe(`${Formatter.format(expected, format)}\n`);
expect(projectionSource[0]?.rows[0]?.retained).toEqual([0, 10]);
},
);
@@ -406,10 +408,9 @@ it.each(["json", "jsonl", "yaml", "toon", "md"] as const)(
.split("\n")
.map((line) => JSON.parse(line)),
).toEqual(expected);
else if (format === "md") {
expect(outputs[0]).toContain(`| ${JSON.stringify(expected[0]!.rows)} |`);
expect(outputs[0]).not.toContain("[object Object]");
} else expect(outputs[0]).toBe(`${Formatter.format(expected, format)}\n`);
else if (format === "md")
expect(outputs[0]).toBe(renderCliDocument(expected, format, false));
else expect(outputs[0]).toBe(`${Formatter.format(expected, format)}\n`);
expect(projectionSource).toEqual(before);
},
);
@@ -0,0 +1,105 @@
import { readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { parseFragment, type DefaultTreeAdapterTypes } from "parse5";
import { createMarkdownRenderer, disposeMdItInstance } from "vitepress";
import { afterAll, expect } from "vitest";
import { createEvidence } from "../../../src/domain/evidence.js";
import { createEvidenceBundle } from "../../../src/domain/evidenceBundle.js";
import { renderCliDocument } from "../../../src/cliOutput.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
afterAll(() => disposeMdItInstance());
const renderedCells = (html: string): string[] => {
const text = (node: DefaultTreeAdapterTypes.Node): string =>
node.nodeName === "#text" && "value" in node
? node.value
: "childNodes" in node
? node.childNodes.map(text).join("")
: "";
const cells: string[] = [];
const visit = (node: DefaultTreeAdapterTypes.Node): void => {
if ("tagName" in node && (node.tagName === "th" || node.tagName === "td"))
cells.push(text(node));
else if ("childNodes" in node) node.childNodes.forEach(visit);
};
visit(parseFragment(html));
return cells;
};
cliTest(
"preserves literal Evidence keys and values when Markdown tables are rendered",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-markdown-literal-cells-");
const path = join(root, "bundle.json");
const literal =
"`code` **bold** _name_ ~~deleted~~ [label](target) ![alt](image)";
const punctuation = "\\`tick` \\*star* \\[label](target) a|b <T> &copy;";
const identifiers = "x__y__z _a_b_ 中_文 snake_case_";
const row = {
[literal]: literal,
punctuation,
nested: { text: literal, values: ["__name__", "plain"] },
empty: "",
zero: 0,
disabled: false,
absent: null,
source_index: identifiers,
};
const result = { scalar: { [literal]: literal }, rows: [row] };
const record = createEvidence(
undefined,
{ id: "fixture", name: "Fixture", version: "1" },
{ operation: "ordinary-json", parameters: {}, result },
);
const saved = JSON.stringify(createEvidenceBundle([record]));
await writeFile(path, saved);
const base = [
"inspect-evidence-bundle",
JSON.stringify({ path, detail: "complete" }),
"--filter-output",
"records.0.normalized_result",
];
const json = await cli.run({ arguments: [...base, "--json"] });
const markdown = await cli.run({ arguments: [...base, "--format", "md"] });
expect(json.exitCode, json.stderr).toBe(0);
expect(markdown.exitCode, markdown.stderr).toBe(0);
expect(JSON.parse(json.stdout)).toEqual({
records: { "0": { normalized_result: result } },
});
expect(await readFile(path, "utf8")).toBe(saved);
// Intraword underscores cannot form emphasis, so identifiers stay readable.
expect(markdown.stdout).toContain(" source_index ");
expect(markdown.stdout).toContain(
" x__y__z \\_a_b\\_ 中_文 snake_case\\_ ",
);
const renderer = await createMarkdownRenderer(root, { linkify: false });
const html = renderer.render(markdown.stdout);
const expectedCells = [
"Key",
"Value",
literal,
literal,
...Object.keys(row),
literal,
punctuation,
JSON.stringify(row.nested),
"",
"0",
"false",
"null",
identifiers,
];
expect(renderedCells(html)).toEqual(expectedCells);
expect(html).not.toMatch(/<(?:em|strong|s|code|img)(?:\s|>)/u);
// Standalone reports share the same table owner as command output.
expect(
renderedCells(renderer.render(renderCliDocument(result, "md", false))),
).toEqual(expectedCells);
},
);
@@ -16,7 +16,7 @@ it("retains nested facts in standalone Markdown table cells and mixed arrays", (
];
const markdown = renderCliDocument(value, "md", false);
expect(markdown).toContain('{"path":"alpha.txt"}');
expect(markdown).toContain('[{"path":"sub/beta.txt"}]');
expect(markdown).toContain('\\[{"path":"sub/beta.txt"}\\]');
expect(markdown).toContain("caller\\|value");
expect(markdown).not.toContain("[object Object]");
expect(
@@ -107,6 +107,7 @@ beforeAll(async () => {
author("claude"),
author("cursoragent"),
author("codex"),
author("Copilot"),
author("morluto"),
]),
associatedPullRequests: page([]),
@@ -235,14 +236,16 @@ it("inventories side branches and paginated credits, then checks and renders syn
inventory.commits.flatMap(({ authors }) =>
authors.map(({ user }) => user?.login),
),
).toEqual(expect.arrayContaining(["claude", "cursoragent", "codex"]));
).toEqual(
expect.arrayContaining(["claude", "cursoragent", "codex", "Copilot"]),
);
expect(inventory.pullRequests.map((pr) => pr.number)).toEqual([101]);
expect(f.record).toContain(
"Thanks @alice, @bob, @reporter, @second-reporter.",
);
expect(f.record).toContain("Thanks @dana.");
expect(f.record).not.toMatch(
/@(?:merger|automation|platformbot|claude|cursoragent|codex|morluto)/u,
/@(?:merger|automation|platformbot|claude|cursoragent|codex|Copilot|morluto)/u,
);
const body = join(f.directory, "body.md");
await writeFile(
@@ -340,6 +343,7 @@ it("rejects lost grouped credits, fabricated references, bots, and stale PR bodi
f.bullet + " Thanks @automation[bot].",
"Unverified human credit @automation[bot]",
],
[f.bullet + " Thanks @Copilot.", "Unverified human credit @copilot"],
]) {
await writeFile(
join(f.directory, "CHANGELOG.md"),
+141 -24
View File
@@ -124,21 +124,24 @@ function historyPage(
};
}
function releasePage() {
function releaseRecord(version: string, sha = baseline) {
const tagName = `rea-agents-${version}`;
return {
name: version,
tag: { name: tagName },
tagCommit: { oid: sha },
description: "",
isDraft: false,
url: `https://github.com/fixture/rea/releases/tag/${tagName}`,
};
}
function releasePage(releases: ReturnType<typeof releaseRecord>[]) {
return {
data: {
repository: {
releases: {
nodes: [
{
name: "6.7.0",
tag: { name: "rea-agents-6.7.0" },
tagCommit: { oid: baseline },
description: "",
isDraft: false,
url: "https://github.com/fixture/rea/releases/tag/rea-agents-6.7.0",
},
],
nodes: releases,
pageInfo: { hasNextPage: false },
},
},
@@ -158,6 +161,29 @@ const defaultConfiguration = {
},
};
function releaseFileEntries(
configuration: Record<string, unknown>,
releasedVersions: Record<string, string>,
) {
const files: Record<string, unknown> = {
"release-please-config.json": configuration,
".release-please-manifest.json": releasedVersions,
"package.json": {
name: "rea-agents",
version: releasedVersions["."] ?? "0.0.0",
},
"pkg/package.json": {
name: "rea-agents",
version: releasedVersions.pkg ?? "0.0.0",
},
};
return Object.entries(files).map(([path, value], index) => ({
path,
sha: (index + 1).toString(16).padStart(40, "0"),
text: JSON.stringify(value),
}));
}
async function releaseFixture({
config = {},
commits = [
@@ -168,6 +194,9 @@ async function releaseFixture({
pullRequest = false,
historyFailures = [],
releaseFailures = [],
releasedVersions = { ".": "6.7.0", pkg: "6.7.0" },
releases = [releaseRecord("6.7.0")],
tags = [],
}: {
config?: Record<string, unknown>;
commits?: CommitRecord[];
@@ -175,6 +204,9 @@ async function releaseFixture({
pullRequest?: boolean;
historyFailures?: HistoryFailure[];
releaseFailures?: HistoryFailure["errors"][];
releasedVersions?: Record<string, string>;
releases?: ReturnType<typeof releaseRecord>[];
tags?: { name: string; commit: { sha: string } }[];
} = {}) {
const requests: { method: string; pathname: string }[] = [];
const pages: HistoryVariables[] = [];
@@ -182,6 +214,7 @@ async function releaseFixture({
const failures = [...historyFailures];
const releaseErrors = [...releaseFailures];
const configuration = { ...defaultConfiguration, ...config };
const entries = releaseFileEntries(configuration, releasedVersions);
const github = await upstream.GitHub.create({
owner: "fixture",
repo: "rea",
@@ -196,7 +229,7 @@ async function releaseFixture({
method: options.method ?? "GET",
pathname: url.pathname,
});
if (url.pathname === "/repos/fixture/rea/tags") return respond([]);
if (url.pathname === "/repos/fixture/rea/tags") return respond(tags);
if (url.pathname === "/repos/fixture/rea/releases/generate-notes") {
return respond({ body: "Generated release notes" });
}
@@ -214,7 +247,7 @@ async function releaseFixture({
if (query.query.includes("query releases(")) {
const errors = releaseErrors.shift();
if (errors) return respond({ errors });
return respond(releasePage());
return respond(releasePage(releases));
}
if (!query.query.includes("query pullRequestsSince(")) {
throw new Error(`Unexpected GraphQL operation: ${query.query}`);
@@ -234,17 +267,6 @@ async function releaseFixture({
}
return respond(historyPage(commits, query.variables, pullRequest));
}
const files: Record<string, unknown> = {
"release-please-config.json": configuration,
".release-please-manifest.json": { ".": "6.7.0", pkg: "6.7.0" },
"package.json": { name: "rea-agents", version: "6.7.0" },
"pkg/package.json": { name: "rea-agents", version: "6.7.0" },
};
const entries = Object.entries(files).map(([path, value], index) => ({
path,
sha: (index + 1).toString(16).padStart(40, "0"),
text: JSON.stringify(value),
}));
if (url.pathname === "/repos/fixture/rea/git/trees/main") {
return respond({
truncated: false,
@@ -503,6 +525,100 @@ it("collects beyond 500 commits and rejects an unobserved known release boundary
).toBe(true);
});
it("rejects a missing manifest release before replaying a historical version override", async () => {
const fixture = await releaseFixture({
releasedVersions: { ".": "6.8.0" },
releases: [releaseRecord("6.7.0", "d".repeat(40))],
commits: [
{ sha: latest, message: "fix: current change", files: ["src/change.ts"] },
{
sha: baseline,
message: "chore: release 6.8.0",
files: ["package.json"],
},
{
sha: "c".repeat(40),
message: "fix: historical version override\n\nRelease-As: 0.4.0",
files: ["src/historical.ts"],
},
],
});
await expect(fixture.manifest.createPullRequests()).rejects.toThrow(
"Missing expected release",
);
expect(fixture.pages).toEqual([]);
expect(
fixture.requests.every(
(request) => request.method === "GET" || request.pathname === "/graphql",
),
).toBe(true);
});
it("uses a tag-only manifest release and excludes older version overrides", async () => {
const fixture = await releaseFixture({
releasedVersions: { ".": "6.8.0" },
releases: [],
tags: [{ name: "rea-agents-6.8.0", commit: { sha: baseline } }],
commits: [
{ sha: latest, message: "fix: current change", files: ["src/change.ts"] },
{
sha: baseline,
message: "chore: release 6.8.0",
files: ["package.json"],
},
{
sha: "c".repeat(40),
message: "fix: historical version override\n\nRelease-As: 0.4.0",
files: ["src/historical.ts"],
},
],
});
const candidates = await fixture.manifest.buildPullRequests();
expect(candidates).toHaveLength(1);
expect(candidates[0]?.body.toString()).toContain("current change");
expect(candidates[0]?.body.toString()).not.toContain(
"historical version override",
);
const version = candidates[0]?.updates.find(
(update) => update.path === "version.txt",
);
expect(
version?.updater.updateContent("6.8.0 // x-release-please-version"),
).toContain("6.9.0");
expect(
fixture.requests.some(
(request) => request.pathname === "/repos/fixture/rea/tags",
),
).toBe(true);
});
it.each(["omitted", "0.0.0"])(
"preserves initial bootstrap with an %s manifest version",
async (version) => {
const releasedVersions: Record<string, string> =
version === "omitted" ? {} : { ".": version };
const fixture = await releaseFixture({
releasedVersions,
releases: [],
commits: [
{
sha: latest,
message: "feat: initial change",
files: ["src/change.ts"],
},
],
});
const candidates = await fixture.manifest.buildPullRequests();
expect(candidates).toHaveLength(1);
const versionUpdate = candidates[0]?.updates.find(
(update) => update.path === "version.txt",
);
expect(
versionUpdate?.updater.updateContent("0.0.0 // x-release-please-version"),
).toContain("1.0.0");
},
);
it.each([
{
name: "timestamped GraphQL internal error",
@@ -637,6 +753,7 @@ it("preserves a reached explicit last-release override", async () => {
it("preserves complete mixed bootstrap history with all known release boundaries", async () => {
const fixture = await releaseFixture({
releasedVersions: { ".": "6.7.0", pkg: "0.0.0" },
config: {
packages: {
".": { "package-name": "rea-agents" },
+51 -10
View File
@@ -21,6 +21,7 @@ import {
occurrenceIdForLocation,
} from "../../../src/domain/artifactIdentity.js";
import { artifactInspectionResultSchema } from "../../../src/domain/artifactInspection.js";
import { parseArtifactInventoryEvidence } from "../../../src/domain/artifactInventoryEvidence.js";
import {
ok as resultOk,
err as resultErr,
@@ -1258,6 +1259,9 @@ it("attributes ASAR integrity contradictions to their actual inputs and withhold
const root = await createTestTempDirectory("rea-asar-attribution-");
const source = join(root, "source");
await mkdir(source);
await writeFile(join(source, "keep.txt"), "unchanged sibling\n");
const absentArchive = join(root, "absent.asar");
await createPackageWithOptions(source, absentArchive, {});
const original = "console.log('ok');\n";
const changed = "console.log('no');\n";
await writeFile(join(source, "main.js"), original);
@@ -1280,7 +1284,12 @@ it("attributes ASAR integrity contradictions to their actual inputs and withhold
await server.connect(st);
await client.connect(ct);
const observations = [];
for (const path of [originalArchive, staleArchive, currentArchive]) {
for (const path of [
originalArchive,
staleArchive,
currentArchive,
absentArchive,
]) {
expect(
(await client.callTool({ name: "open_binary", arguments: { path } }))
.isError,
@@ -1298,20 +1307,32 @@ it("attributes ASAR integrity contradictions to their actual inputs and withhold
PROCESS_COMPARISON_EVIDENCE,
])
expect(session.recordEvidence(record).ok).toBe(true);
const [originalEvidence, stale, current] = observations;
const [originalEvidence, stale, current, absent] = observations;
if (
originalEvidence === undefined ||
stale === undefined ||
current === undefined
current === undefined ||
absent === undefined
)
throw new Error("Expected three artifact observations");
for (const [left, right, mismatches] of [
[stale, current, [stale.evidence_id]],
[current, stale, [stale.evidence_id]],
[stale, stale, [stale.evidence_id]],
[originalEvidence, current, []],
throw new Error("Expected four artifact observations");
for (const [left, right, mismatches, presence] of [
[stale, current, [stale.evidence_id], "changed"],
[current, stale, [stale.evidence_id], "changed"],
[stale, stale, [stale.evidence_id], "changed"],
[originalEvidence, current, [], "changed"],
[absent, stale, [stale.evidence_id], "added"],
[stale, absent, [stale.evidence_id], "removed"],
[absent, current, [], "added"],
[current, absent, [], "removed"],
] as const)
await assertIntegrityComparison(client, session, left, right, mismatches);
await assertIntegrityComparison(
client,
session,
left,
right,
mismatches,
presence,
);
} finally {
await Promise.allSettled([client.close(), server.close(), session.close()]);
}
@@ -1323,6 +1344,7 @@ const assertIntegrityComparison = async (
left: ReturnType<typeof toolEvidence>,
right: ReturnType<typeof toolEvidence>,
mismatches: readonly string[],
presence: "added" | "removed" | "changed",
) => {
const compared = await client.callTool({
name: "compare_artifacts",
@@ -1332,12 +1354,31 @@ const assertIntegrityComparison = async (
const comparison = toolEvidence(compared.structuredContent);
expect(comparison.normalized_result).toMatchObject({
status: mismatches.length === 0 ? "changed" : "contradiction",
summary: { contradiction: mismatches.length === 0 ? 0 : 1 },
integrity_mismatch_evidence_ids: mismatches,
});
const unknown = session
.listUnknowns({ domain: "artifact-comparison" })
.find((item) => item.question.includes(comparison.evidence_id));
expect(unknown).toBeDefined();
const [leftMember, rightMember] = [left, right].map((record) =>
parseArtifactInventoryEvidence(record).inventory.occurrences.find(
({ logical_path }) => logical_path === "main.js",
),
);
expect(comparison.normalized_result).toMatchObject({
changes: expect.arrayContaining([
expect.objectContaining({
logical_path: "main.js",
classification: mismatches.length > 0 ? "contradiction" : presence,
...(mismatches.length > 0 ? { dimensions: ["integrity"] } : {}),
left_occurrence_id: leftMember?.occurrence_id ?? null,
left_artifact_id: leftMember?.artifact_id ?? null,
right_occurrence_id: rightMember?.occurrence_id ?? null,
right_artifact_id: rightMember?.artifact_id ?? null,
}),
]),
});
if (mismatches.length > 0) {
expect(unknown?.contradicting_evidence_ids).toEqual(mismatches);
expect(unknown?.supporting_evidence_ids).toEqual(
@@ -24,6 +24,10 @@ import {
renamedBook,
} from "../../fixtures/environmentStateWrites.js";
import { referencedCreateRows } from "../../fixtures/environmentStateDerivedValues.js";
import {
READ_BACK_TAG_ROWS,
slugOnlySaveRows,
} from "../../fixtures/environmentStateWriteViews.js";
import { unsupportedInputLiteralSchemaError } from "../../fixtures/mcpClientSchemaConstraints.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { startListeningProcess } from "../../support/mcp/streamableHttpProcess.js";
@@ -34,6 +38,66 @@ const HTTP_BOUNDARY_TIMEOUT_MS = 90_000;
/** The book the first served create of "Boundary Book" makes: one second past the served clock's start. */
const BOUNDARY_BOOK = book(3, "Boundary Book", bookClock(13));
processTest(
"keeps create response clocks separate from enriched item reads through the SDK",
async ({ processes }) => {
const rows = READ_BACK_TAG_ROWS;
const root = await createTestTempDirectory("rea-write-views-sdk-");
const files = await writeEnvironmentServeFiles(
root,
environmentStateServeFixture(timedCaptureEvidence(rows)),
);
const started = await startListeningProcess(processes, [
"environment-serve",
files.manifestPath,
"--evidence",
files.capturePath,
"--port",
"0",
]);
const client = new Client({ name: "write-views", version: "1" });
await client.connect(new StreamableHTTPClientTransport(started.url));
try {
const { tools } = await client.listTools();
expect(tools.map(({ name }) => name)).toEqual(
expect.arrayContaining(["post_tags", "get_tags_by_id"]),
);
const created = await client.callTool({
name: "post_tags",
arguments: { body: { name: "Boundary Tag" } },
});
expect(created.isError).toBeFalsy();
const clock = bookClock(rows.length + 1);
expect(created.structuredContent).toMatchObject({
body: {
id: 3,
name: "Boundary Tag",
created: clock.replace("Z", ".000000000Z"),
},
state: { source: "state" },
});
expect(created.structuredContent).not.toHaveProperty(
"body.document_count",
);
const read = await client.callTool({
name: "get_tags_by_id",
arguments: { id: "3" },
});
expect(read.structuredContent).toMatchObject({
body: {
id: 3,
name: "Boundary Tag",
created: clock,
document_count: 0,
},
});
} finally {
await client.close();
}
},
HTTP_BOUNDARY_TIMEOUT_MS,
);
processTest(
"discovers empty fixed values and applies omitted recorded queries through the SDK before enforcing writes",
async ({ processes }) => {
@@ -411,15 +475,28 @@ processTest(
HTTP_BOUNDARY_TIMEOUT_MS,
);
processTest(
"withholds a create from public tools when its sent reference contradicts the recorded response",
async ({ processes }) => {
const root = await createTestTempDirectory("rea-create-reference-mcp-");
const rows = referencedCreateRows({
processTest.for([
{
label: "a create whose sent reference contradicts its response",
rows: referencedCreateRows({
id: 1,
name: "Different Hall",
floor: "ground",
});
}),
read: "get_books",
write: "post_books",
},
...[true, false].map((derivedFirst) => ({
label: `a slug-only edit incompatible with a derived save (derived first: ${String(derivedFirst)})`,
rows: slugOnlySaveRows(derivedFirst, true),
read: "get_tags",
write: "patch_tags_by_id",
})),
])(
"withholds $label from public tools",
{ timeout: HTTP_BOUNDARY_TIMEOUT_MS },
async ({ rows, read, write }, { processes }) => {
const root = await createTestTempDirectory("rea-create-reference-mcp-");
const files = await writeEnvironmentServeFiles(
root,
environmentStateServeFixture(timedCaptureEvidence(rows)),
@@ -439,11 +516,10 @@ processTest(
await client.connect(new StreamableHTTPClientTransport(started.url));
try {
const { tools } = await client.listTools();
expect(tools.map(({ name }) => name)).toContain("get_books");
expect(tools.map(({ name }) => name)).not.toContain("post_books");
expect(tools.map(({ name }) => name)).toContain(read);
expect(tools.map(({ name }) => name)).not.toContain(write);
} finally {
await client.close();
}
},
HTTP_BOUNDARY_TIMEOUT_MS,
);
+292
View File
@@ -0,0 +1,292 @@
import type { TimedCaptureRow } from "./analysisView.js";
import { bookClock } from "./environmentStateWrites.js";
const tag = (id: number, name: string, slug: string, created: string) => ({
id,
slug,
name,
document_count: id === 1 ? 2 : 0,
created,
});
const SEED_TAGS = [
tag(1, "Lamp Oil", "lamp-oil", "2026-10-01T09:00:00Z"),
tag(2, "Tide Tables", "tide-tables", "2026-10-01T10:00:00Z"),
];
/** A tag as a create answers it: without the document count reads show, and with its clock to the nanosecond. */
const answered = (id: number, name: string, slug: string, at: number) => ({
id,
slug,
name,
created: bookClock(at).replace("Z", ".123456789Z"),
});
/** Two tag creates whose answers lack the document count, and spell the clock otherwise, than the item read after each. */
export const READ_BACK_TAG_ROWS: readonly TimedCaptureRow[] = [
["GET", "/api/tags", SEED_TAGS],
["GET", "/api/tags/1", SEED_TAGS[0]!],
["GET", "/api/tags/2", SEED_TAGS[1]!],
[
"POST",
"/api/tags",
answered(3, "Harbour Notices", "harbour-notices", 3),
201,
{ name: "Harbour Notices" },
],
[
"GET",
"/api/tags/3",
tag(3, "Harbour Notices", "harbour-notices", bookClock(3)),
],
[
"GET",
"/api/tags",
[...SEED_TAGS, tag(3, "Harbour Notices", "harbour-notices", bookClock(3))],
],
[
"POST",
"/api/tags",
answered(4, "Quarry Notes", "quarry-notes", 6),
201,
{ name: "Quarry Notes" },
],
["GET", "/api/tags/4", tag(4, "Quarry Notes", "quarry-notes", bookClock(6))],
[
"GET",
"/api/tags",
[
...SEED_TAGS,
tag(3, "Harbour Notices", "harbour-notices", bookClock(3)),
tag(4, "Quarry Notes", "quarry-notes", bookClock(6)),
],
],
];
const label = (id: number, title: string, posts: number) => ({
id,
title,
count: { posts },
});
/** Two label creates whose answers lack the post count only the list shows; no item read follows them. */
export const LISTED_LABEL_ROWS: readonly TimedCaptureRow[] = [
["GET", "/api/labels", [label(1, "Ferry", 3), label(2, "Pier", 1)]],
[
"POST",
"/api/labels",
{ id: 3, title: "Mooring" },
201,
{ title: "Mooring" },
],
[
"GET",
"/api/labels",
[label(1, "Ferry", 3), label(2, "Pier", 1), label(3, "Mooring", 0)],
],
["POST", "/api/labels", { id: 4, title: "Buoy" }, 201, { title: "Buoy" }],
[
"GET",
"/api/labels",
[
label(1, "Ferry", 3),
label(2, "Pier", 1),
label(3, "Mooring", 0),
label(4, "Buoy", 0),
],
],
];
/** Two renames that send the tag whole, its old slug included, which the application slugs anew from the name. */
export const STALE_SLUG_ROWS: readonly TimedCaptureRow[] = [
["GET", "/api/tags", SEED_TAGS],
["GET", "/api/tags/1", SEED_TAGS[0]!],
["GET", "/api/tags/2", SEED_TAGS[1]!],
[
"PUT",
"/api/tags/1",
{
...SEED_TAGS[0]!,
name: "Lamp Oil Deliveries",
slug: "lamp-oil-deliveries",
},
200,
{ ...SEED_TAGS[0]!, name: "Lamp Oil Deliveries" },
],
[
"GET",
"/api/tags/1",
{
...SEED_TAGS[0]!,
name: "Lamp Oil Deliveries",
slug: "lamp-oil-deliveries",
},
],
[
"PUT",
"/api/tags/2",
{
...SEED_TAGS[1]!,
name: "Spring Tide Tables",
slug: "spring-tide-tables",
},
200,
{ ...SEED_TAGS[1]!, name: "Spring Tide Tables" },
],
[
"GET",
"/api/tags/2",
{
...SEED_TAGS[1]!,
name: "Spring Tide Tables",
slug: "spring-tide-tables",
},
],
[
"GET",
"/api/tags",
[
{
...SEED_TAGS[0]!,
name: "Lamp Oil Deliveries",
slug: "lamp-oil-deliveries",
},
{
...SEED_TAGS[1]!,
name: "Spring Tide Tables",
slug: "spring-tide-tables",
},
],
],
];
const POST = {
id: 1,
title: "Winter rota",
updated_at: "2026-10-01T09:00:00Z",
};
/** Two edits that send back the modification time they read, which the application stamps anew. */
export const RESTAMPED_ROWS: readonly TimedCaptureRow[] = [
["GET", "/api/posts", [POST]],
["GET", "/api/posts/1", POST],
[
"PUT",
"/api/posts/1",
{ ...POST, title: "Winter rota v2", updated_at: bookClock(2) },
200,
{ ...POST, title: "Winter rota v2" },
],
[
"GET",
"/api/posts/1",
{ ...POST, title: "Winter rota v2", updated_at: bookClock(2) },
],
[
"PUT",
"/api/posts/1",
{ ...POST, title: "Winter rota v3", updated_at: bookClock(4) },
200,
{ ...POST, title: "Winter rota v3", updated_at: bookClock(2) },
],
[
"GET",
"/api/posts/1",
{ ...POST, title: "Winter rota v3", updated_at: bookClock(4) },
],
[
"GET",
"/api/posts",
[{ ...POST, title: "Winter rota v3", updated_at: bookClock(4) }],
],
];
/** One save leaves its slug as sent; the other demonstrates regeneration, in either recording order. */
export const slugSaveRows = (
derivedFirst: boolean,
customSlug = false,
): readonly TimedCaptureRow[] => {
const saved = { ...SEED_TAGS[0]!, ...(customSlug ? { slug: "custom" } : {}) };
const renamed = {
...SEED_TAGS[1]!,
name: "Spring Tide Tables",
slug: "spring-tide-tables",
};
const saves: TimedCaptureRow[][] = [
[
["PUT", "/api/tags/1", saved, 200, saved],
["GET", "/api/tags/1", saved],
],
[
[
"PUT",
"/api/tags/2",
renamed,
200,
{ ...SEED_TAGS[1]!, name: renamed.name },
],
["GET", "/api/tags/2", renamed],
],
];
return [
...STALE_SLUG_ROWS.slice(0, 3),
...saves[derivedFirst ? 1 : 0]!,
...saves[derivedFirst ? 0 : 1]!,
["GET", "/api/tags", [saved, renamed]],
];
};
/** A slug-only PATCH and a save that demonstrates deriving the slug from the name. */
export const slugOnlySaveRows = (
derivedFirst: boolean,
changed: boolean,
): readonly TimedCaptureRow[] =>
slugSaveRows(derivedFirst, changed).map(
([method, path, body = null, status, request, ...rest]): TimedCaptureRow =>
method === "PUT"
? [
"PATCH",
path,
body,
status ?? 200,
path === "/api/tags/1"
? { slug: changed ? "custom" : "lamp-oil" }
: request,
...rest,
]
: [method, path, body, status ?? 200, request, ...rest],
);
/** One whole-object save falls in its held clock's second; the other visibly stamps a fresh clock. */
export const clockSaveRows = (
derivedFirst: boolean,
): readonly TimedCaptureRow[] => {
const heldIndex = derivedFirst ? 5 : 3;
const freshIndex = derivedFirst ? 3 : 5;
const held = { ...POST, updated_at: bookClock(heldIndex) };
const fresh = { ...POST, id: 2 };
const saved = { ...held, title: "Same-second save" };
const stamped = {
...fresh,
title: "Fresh save",
updated_at: bookClock(freshIndex),
};
const saves: TimedCaptureRow[][] = [
[
["PUT", "/api/posts/1", saved, 200, saved],
["GET", "/api/posts/1", saved],
],
[
["PUT", "/api/posts/2", stamped, 200, { ...fresh, title: stamped.title }],
["GET", "/api/posts/2", stamped],
],
];
return [
["GET", "/api/posts", [held, fresh]],
["GET", "/api/posts/1", held],
["GET", "/api/posts/2", fresh],
...saves[derivedFirst ? 1 : 0]!,
...saves[derivedFirst ? 0 : 1]!,
["GET", "/api/posts", [saved, stamped]],
];
};