diff --git a/.agents/skills/reverse-engineer-anything/references/evidence-workflows.md b/.agents/skills/reverse-engineer-anything/references/evidence-workflows.md index 90cf08eeb3..fe542fa90d 100644 --- a/.agents/skills/reverse-engineer-anything/references/evidence-workflows.md +++ b/.agents/skills/reverse-engineer-anything/references/evidence-workflows.md @@ -104,3 +104,14 @@ each plan states why in its `reason`. It replays the capture instead with `--replay-only` or when the state is unavailable (not derivable, or a catalog over the discovery budget), and its stopped document's `state` says which applied. + +For a PE/ELF export question, `inspect_native_exports` accepts `detail: "summary"` +and retains the complete table. Query `inspect_analysis_view` with +`view: {kind: "native-exports", collection: "exports", names: ["selected_name"], +fields: ["name"], offset: 0, limit: 32}` and the reported parent Evidence ID. +All same-name versions and aliases survive; use `source_index` with the parent ID +for stable identity. `collection: "issues"` pages interpretation failures. Page +coverage describes selected retained rows; `summary.coverage` or `exports.coverage` +still describes the source table, so an empty partial page does not prove absence. +CLI `inspect-native-exports --detail summary --evidence_output PATH` saves the +complete bundle for later `inspect-analysis-view --evidence-bundle PATH` requests. diff --git a/docs/agent-environments.md b/docs/agent-environments.md index 5d03d9fca2..a929c1abf8 100644 --- a/docs/agent-environments.md +++ b/docs/agent-environments.md @@ -777,11 +777,27 @@ two collections can reuse a live id, and an equal pair stays in that collection. environment answers, a path parameter by the collection the live path names (a nested collection keeps its parent values, each decoded once, and a literal route segment stays), except a query value that pages a list, such as `limit` or `page`. Each call is checked against the served tool's input schema first, as the MCP server checks it. After mapping, two clock values compare by format only (ISO-8601 strings, and epoch numbers under `at`, `time`, `date`, `timestamp`, `created`, `updated` or `modified` field names, including snake-case and camel-case forms). -Numeric array elements have no time-named field and remain literal measurements. -Different string values compare as independently generated only at entity fields -whose executed served write plan declares a unique token or UUID/token identity -generator. UUID-like or token-like spelling alone does not establish generation; -other business strings compare literally. The report counts each kind of normalised value. +Numeric arrays retain that containing field's clock context through nested +arrays; an object's own fields establish their own context. Numeric values at +the body root, in root arrays, or under measurement fields remain literal. + +Generated strings normalize only where this confirmed, served write's entity +response rules identify generation. A `unique` rule or UUID identity rule must +still compare two UUIDs; a `renewed` rule applies only at its recorded pointers, +with `*` matching actual array items. Token/sequence strings retain the existing +format check: equal length of at least eight, the same character classes, and +a digit. Numeric sequences continue to compare literally unless an ID mapping +applies. String shape and names such as `token` or `sku` alone do not establish +generation. Read and replay responses without these facts stay literal except +for supported clocks and learned IDs. A missing write target replays an absence +response and does not inherit the successful entity response's generation rules. +Admitted generated values must also preserve aliases and distinct identities +within that response. UUID identity uses the generator's case-insensitive +matching; token and sequence strings retain their literal identity. This +correspondence ends with the answer and is not inferred across calls. +The report counts each kind of normalised +value; the generation rules remain model-derived facts, rather than new +observations of the application. ISO clock offsets are accepted only within RFC 3339's `00`–`23` hour and `00`–`59` minute ranges. Probe ordering retains every recorded fractional-second digit in ISO timestamps with a four-digit year, `T`, and a `Z` or `±HH:MM` suffix. Equal instants use ordinal order. Other native-Date diff --git a/docs/apk-signature.md b/docs/apk-signature.md index f161969e2b..ffdcfc0e10 100644 --- a/docs/apk-signature.md +++ b/docs/apk-signature.md @@ -221,9 +221,9 @@ Not yet read: v4 `.apk.idsig` (a sibling file), verity (`0x0421`/`0x0423`/`0x042 ## Bounds and errors The archive is read once, bounded at 512 MiB, without following symlinks and -with change detection. Every size and offset in the ZIP tail and signing block -is checked against its containing section before use, including the complete -central-directory extent before ZIP end records. A process-wide queue permits +with change detection. ZIP tail offsets, the central-directory size and signing +block spans used by the reader are checked against their containing sections, +including the complete central-directory extent before ZIP end records. A process-wide queue permits one active whole-file inspection and eight pending scans across provider instances; pending scans retain no APK bytes and can be cancelled with their original reason. Further requests receive a capacity error. The active slot @@ -250,8 +250,17 @@ A file that is not a ZIP is refused as an unsupported target. A damaged ZIP (local headers but no end-of-central-directory), a central directory that cannot be walked or whose declared record count does not account for all of its bytes, or a malformed signing block or signer is an input error naming the -scheme and reason. Signer framing includes signature records, the public-key -field and matching signed/outer v3 SDK ranges. Suffix bytes beyond known +offset or scheme and reason. Zip64 tail damage includes a missing required +locator, an impossible locator disk/count pair, a fixed end record that does +not fit before its locator, or an incorrect end-record signature. These are +input errors, not evidence of an unsupported format. Coherent multi-disk +locator metadata and 64-bit offsets/counts/sizes beyond the reader's safe +integer range remain unsupported. The locator offset is disk-relative, so +multi-disk metadata is checked before applying local file bounds. This follows +the record fields in [PKWARE APPNOTE 6.3.10, sections 4.3.14–4.3.15](https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT); +it does not establish full validation of Zip64 versions or extensible data. +Signer framing includes signature records, the public-key field and matching +signed/outer v3 SDK ranges. Suffix bytes beyond known fields are counted as uninterpreted, as Android readers may ignore extensions; their presence does not establish malformed signing. When the central directory is not immediately followed by diff --git a/docs/architecture.mermaid b/docs/architecture.mermaid index ddb1d785b4..6ab14e680c 100644 --- a/docs/architecture.mermaid +++ b/docs/architecture.mermaid @@ -38,7 +38,11 @@ graph TD IDA["src/ida/
Upstream MCP contract adaptation
live GUI binding or owned headless database"] KUNA["src/kuna/
BYO Kuna deep analysis candidate
discovery reads metadata; version probe on target open"] IDA_MCP["External ida-pro-mcp
GUI proxy or database supervisor
upstream installation and engine lifecycle"] + + KUNA["src/kuna/
BYO Kuna decompiler, a deep analysis candidate
discovery reads metadata only; version probe when a target opens"] AUX["src/artifacts/ + src/native/
artifact inventory, bounded directory triage and extraction
format readers and native capabilities"] + + BROWSER["src/browser/
CDP / Playwright capture
captured scripts and native module resolution
execution/: coverage, listener and source attribution"] JS_CODEC["src/javascript/sourceMaps/
pinned upstream map codec
owned heap-limited Node process, complete point evidence"] INSPECTOR["src/inspector/
passive V8 Inspector discovery and capture
script and target locations"] @@ -50,12 +54,19 @@ graph TD FIRMWARE["src/firmware/
Binwalk / Unblob adapters
owned processes, report normalization, private extraction"] ANDROID["src/android/
Static APK adapter
owned SDK stdio, pinned headless JADX, normalization"] REFERENCE["src/reference/
Filesystem reference-source reader
bounded traversal, validation, errors"] + + ANDROID_TOOLS["src/adb/ + src/apktool/ + src/jeb/
BYO device, resource-decoding and JEB adapters
owned adb and Apktool processes; JEB engine client over MCP"] + CAPTURE_TOOLS["src/captures/ + src/pcap/ + src/signals/ + src/sqlite/
BYO tshark, pcap, sigrok and SQLite child inspection
selected captures, recordings and databases; owned processes"] + NATIVE_READERS["src/go/ + src/rust/ + src/il2cpp/ + src/flutter/ + src/gpu/
Bundled in-process binary metadata readers
Go build info, Rust dependencies, IL2CPP, Dart build ID, GPU code objects
src/compression/: shared codec-result helper for rust and gpu"] + FORMAT_READERS["src/model/ + src/python/ + src/erlang/ + src/electron/
Bundled in-process format readers
GGUF and safetensors headers, Python .pyc, Erlang BEAM, Electron fuses"] + RUNTIME["src/frida/ + src/cutter/
Frida instrumentation sessions and live Cutter bridge client"] end subgraph "Provider Process Foundation" PROCESS["src/process/
owned process groups, private roots
deadlines, bounded file reads and diagnostics, cleanup"] WINDOWS["src/windows/
Windows host boundary adapters
package loader, opaque handles, owned jobs and private runtimes"] WINDOWS_NATIVE["native/windows/src/
Node-API implementation
NTFS admission, protected DACLs and atomic Job Object assignment"] + FILESYSTEM["src/filesystem/
owned file and directory handles
stable reads, staged output, caller paths"] end subgraph "Contracts Layer" @@ -181,3 +192,23 @@ graph TD APP -->|EVM interface port| EVM EVM --> PROCESS EVM --> DOMAIN + COMPOSITION --> KUNA + KUNA --> PROCESS + KUNA --> FILESYSTEM + KUNA --> DOMAIN + COMPOSITION --> ANDROID_TOOLS + ANDROID_TOOLS --> PROCESS + ANDROID_TOOLS --> DOMAIN + COMPOSITION --> CAPTURE_TOOLS + CAPTURE_TOOLS --> PROCESS + CAPTURE_TOOLS --> DOMAIN + COMPOSITION --> NATIVE_READERS + NATIVE_READERS --> DOMAIN + COMPOSITION --> FORMAT_READERS + FORMAT_READERS --> DOMAIN + SERVER --> RUNTIME + CLI --> RUNTIME + RUNTIME --> DOMAIN + PROCESS --> FILESYSTEM + AUX --> FILESYSTEM + APP --> FILESYSTEM diff --git a/docs/binary-diagnostics.md b/docs/binary-diagnostics.md index 857cd53830..242949f5b0 100644 --- a/docs/binary-diagnostics.md +++ b/docs/binary-diagnostics.md @@ -340,7 +340,48 @@ rea inspect-native-exports ./runtimes/linux-x64/native/libexample.so --json ``` The complete Evidence identifies the stable artifact snapshot by path, byte -count and SHA-256. +count and SHA-256. Complete delivery remains the default. For a small initial +MCP reply, select `detail: "summary"`; this retains the complete Evidence and +returns artifact identity, table coverage and row counts. Use its +`normalized_result.parent_evidence_id` with `inspect_analysis_view`: + +```json +{ + "source": { + "kind": "retained-evidence", + "evidence_id": "ev_" + }, + "view": { + "kind": "native-exports", + "collection": "exports", + "names": ["selected_name"], + "fields": ["name"], + "offset": 0, + "limit": 32 + } +} +``` + +Names match exactly and preserve every same-name version or alias. Optional +`indices` selects original row positions, intersecting a name filter when both +are supplied; empty filters select nothing. `source_index` always remains and, +with the parent ID, identifies the original export or issue row. Fields default +to the complete row; an empty field list returns only its identity. ELF `name` +selection also returns raw-byte and encoding qualifications. Fields from the +other format are rejected. `collection: "issues"` pages the complete diagnostic +records, with optional original indices. Offset and limit default to 0 and 64 +and apply to matching rows in original order. + +View `coverage` describes the selected page; `summary.coverage` or +`exports.coverage` preserves source-table coverage. An exhausted or empty page +does not make a partial source complete. Retained views never reopen the file. +The CLI uses the same projector and requires a durable bundle for a summary: + +```sh +rea inspect-native-exports ./libexample.so --detail summary --evidence_output ./exports.bundle.json --json +# Pass the summary's parent ID and the same view as JSON to inspect-analysis-view, +# together with --evidence-bundle ./exports.bundle.json. +``` - PE rows reproduce the export directory: each used export-address-table slot with its biased `ordinal` (`OrdinalBase` plus the slot index), one row per diff --git a/docs/browser-observation.md b/docs/browser-observation.md index dfcdd987c2..a910fbc66a 100644 --- a/docs/browser-observation.md +++ b/docs/browser-observation.md @@ -71,7 +71,7 @@ debugging client, not an REA-owned observing session. The selected page-session collector does not collect worker-session requests, console events, or scripts, and workers that disappeared before the snapshot may be missing; worker coverage is therefore attach-limited. Missing ownership metadata is reported as -`unknown_target_relationship` with unavailable worker attribution, separately +`unattributed_target` with unavailable worker attribution, separately from a proven foreign owner excluded as `out_of_target_scope`. A shared origin alone establishes neither relationship. @@ -462,6 +462,11 @@ Frames, resources, scripts, events, and workers outside the selected origin scop Retained workers include validated opener-target and parent-frame IDs when CDP provides them; these relationships do not expand the exact-origin boundary or prove static module ownership. +Workers without either relationship are withheld with an `unattributed_target` +exclusion and `workers` in `unavailable_sections`: a same-origin URL does not +establish which page owns a worker. A supplied relationship to another target or +frame remains `out_of_target_scope`. Missing ownership also remains unknown when +the worker URL is empty; withheld target details are not returned. ## Completeness and limits diff --git a/docs/electron-observation.md b/docs/electron-observation.md index c6469b15d0..c462b21f78 100644 --- a/docs/electron-observation.md +++ b/docs/electron-observation.md @@ -119,6 +119,11 @@ target, frame, script, and resource, a worker URL must resolve to a local file before it is retained. Relationship IDs improve attribution but do not prove which static module started a worker or that its work completed. +Missing opener and parent relationships leave worker ownership unknown. Such +targets are withheld with an `unattributed_target` exclusion and unavailable +`workers` coverage, even when their URL names a local file. Explicit foreign +relationships remain `out_of_target_scope`; malformed supplied relationship IDs +are `invalid_protocol_value`. Inspection does not retain DOM values, execute renderer code, navigate, click, invoke Electron IPC, close a target, or terminate the application. diff --git a/docs/il2cpp-metadata.md b/docs/il2cpp-metadata.md index 5b9f667282..7793442282 100644 --- a/docs/il2cpp-metadata.md +++ b/docs/il2cpp-metadata.md @@ -214,9 +214,13 @@ assembly image. Each module record starts with its module name, its method pointer count and its method pointer array. The reader finds, for every metadata image, the single record whose name pointer reaches that image's name string and whose count equals the image's method count. It then requires exactly one -`codeGenModulesCount`/`codeGenModules` pair that lists exactly those records. No -symbols are needed, so stripped builds work. A method's token row id selects its -slot in the module's method pointer array. Within one image, each method must +`codeGenModulesCount`/`codeGenModules` pair that lists exactly those records. +`codeGenModulesCount` and module method counts own four bytes, followed by +alignment padding before their eight-byte pointers. Unresolved loader writes +overlapping those four bytes prevent the registration from being established; +padding outside the field is neither required nor interpreted as part of its +count. No symbols are needed, so stripped builds work. A method's token row id +selects its slot in the module's method pointer array. Within one image, each method must claim a distinct MethodDef token row; duplicate rows are refused before any linked method table or seed output is published. Row ids may repeat across different images, and distinct rows may still resolve to the same address when @@ -275,17 +279,27 @@ The result adds: metadata name (`TKey`). Nested generic arguments stay with the component that declares them, for example `Game.Outer+Inner` or `Game.Outer+Leaf` when the nested type adds no parameters. A trailing - `&` marks a by-reference type. `name` is `null` only for type index `-1`. + `&` marks a by-reference type. `name` is `null` for type index `-1`, metadata + version 27 as described below, or an unresolved type-table slot. A dyld bind + or unresolved ELF relocation retains the type index without interpreting its + stored word as a resolved pointer. If an unresolved loader value occurs + inside a type graph, linking is refused with `unsupported-pointer-encoding`; + a concrete name is not synthesized from that placeholder. `binary` also records `types_field` and `types_count`: the address of `Il2CppMetadataRegistration.typesCount` and its value. That record is found as the single place whose `fieldOffsetsCount` and `typeDefinitionsSizesCount` both equal the metadata type-definition count and whose type table is file mapped. Counts use their declared 32-bit width independently of alignment padding. -The complete registration must have mapped coverage; trailing declared zero-fill -fields do not need physical file bytes. All dereferenced pointers use the loader -fixup interpretation: unresolved dyld bindings cannot supply concrete type names -from their stored placeholders and native linking refuses an unavailable type record. +The complete 128-byte registration must have unambiguous loader coverage. Its +used fields through byte 112 must be file-backed; a trailing declared zero-fill +range can cover the final unused fields. An unmapped or conflicting tail is +refused. These layouts follow the versioned [Unity 2022.3.8f1 producer header](https://github.com/MlgmXyysd/libil2cpp/blob/master/libil2cpp/Unity_2022.3/2022.3.8f1/il2cpp-class-internals.h#L596-L637) +(redistributed source) and the [AArch64 aggregate alignment rules](https://github.com/ARM-software/abi-aa/blob/2025Q1/aapcs64/aapcs64.rst#aggregates). +Dyld pointer ownership comes from the parsed bind stream, as in Apple's +[dyld analyzer](https://github.com/apple-oss-distributions/dyld/blob/dyld-1165.3/common/MachOAnalyzer.cpp#L1508-L1511), rather than the slot's placeholder word. +The padding, zero-fill-tail and unresolved-type cases have authored regression +fixtures; they do not add real Unity-build verification to the checks above. Without `binary_path`, `binary` is `null` and the four binary collections are empty in the complete producer report to indicate unrequested observations. Their summary counts, including state counts, are `null`, and native page requests report unavailable observations. This does not establish an empty native inventory. Missing native fields in historical Evidence remain unknown too. diff --git a/docs/process-capture.md b/docs/process-capture.md index 3f6353af2b..52f0ab4d29 100644 --- a/docs/process-capture.md +++ b/docs/process-capture.md @@ -246,7 +246,12 @@ A missing entry proves creation or deletion only when the corresponding observation root's path enumeration was exhausted. Otherwise its effect has `status: "unknown"`, the available before/after state, and a reason; a null state in this variant means unobserved. Paths below symlinks that REA did not -follow also remain unknown. Partial enumeration of one root does not erase +follow also remain unknown. Each observed symlink records a +`symlink_not_followed` enumeration omission with a null system code, including +a stable selected root. The link's own metadata can remain unchanged while +descendant coverage and filesystem comparison remain unknown. Physical +parent-directory enumeration still proves unrelated sibling absence. +Partial enumeration of one root does not erase known effects in another, and unavailable content hashes do not by themselves make path absence unknown. @@ -275,6 +280,12 @@ denied or otherwise unavailable lookup records an `entry_unavailable` omission for that path instead, and its root stays incomplete. Cancellation and unexpected I/O failures still stop capture. +An initial selected-root permission denial returns `access_denied` with the +selected path, native `EACCES` or `EPERM`, and filesystem-read recovery. Capture +does not launch the target. Earlier snapshot facts and the denied root's +omission remain in the partial observation; unresolved read cleanup retains its +own classification until its exact resources are released. + Terminal frame `data` is the comparison text after the selected normalization. New captures also preserve `raw_data` when normalization changes a PTY chunk; otherwise `data` is already the original text. Older captures may lack the diff --git a/docs/reconstruction-coverage.md b/docs/reconstruction-coverage.md index 2d4d132a82..f2f184ad14 100644 --- a/docs/reconstruction-coverage.md +++ b/docs/reconstruction-coverage.md @@ -64,3 +64,17 @@ fails closure even when another proof passes; unknown, unsupported or skipped applicable outcomes keep it unknown. The result cites all applicable outcomes. Proof array order does not establish freshness or supersession. A proof for unrelated artifact digests does not contradict the selected boundary. + +## Function reconstruction claims + +`verify_reconstruction` evaluates declared claims over retained comparison +Evidence. An `overall` function claim includes active residual unknowns scoped +to any of that comparison's dimensions. For example, an open pseudocode +question keeps the overall claim `unknown` and retains its unknown ID and +recommended probe; an independent assembly claim can still pass. An unknown +scoped only to `overall` does not block every individual dimension. + +Selection uses each unknown's latest revision, its comparison identity and +Evidence references. Explicit unknown dependencies remain relevant across +dimensions. Unknowns for another comparison and resolved heads do not become +blockers merely because their source Evidence overlaps. diff --git a/docs/recorded-crashes.md b/docs/recorded-crashes.md index 0eadabad81..5d8a98c55e 100644 --- a/docs/recorded-crashes.md +++ b/docs/recorded-crashes.md @@ -29,6 +29,12 @@ note interpretation; an unspecified OSABI alone does not prove OS origin. from the decoder's `type` label. Source binding and semantic decisions use the numeric identity; an architecture-specific display name does not establish support for that segment's payload semantics. + Header `type` names follow the pinned parser's machine-specific projection: + `PT_AARCH64_ARCHEXT` and `PT_AARCH64_UNWIND` denote `0x70000000` and + `0x70000001` only for AArch64. The same codes on x86-64 remain numeric, as + does AArch64's `0x70000002` in pyelftools 0.33. These display projections + are checked against the architecture and source-bound `type_id`; they do not + decode architecture compatibility, unwind or MTE payloads. - Every note's original identity, owner/descriptor bytes and source ranges, including unfamiliar owners and note kinds. CORE symbolic note labels apply only to the exact `CORE\0` owner. Other @@ -113,8 +119,14 @@ GDB/Python/pwndbg/strace. Fixture generation explicitly launches its owned test program; subsequent inspection only reads the resulting core. Real acceptance checks recorded threads, high registers, missing/malformed notes, opaque owners, CLI/MCP parity, optional debugger context and historical PID collision with an -owned live sentinel. An observed Node thread group must launch the exact ownership -`ps` inventory before it can read that live process's `stat` or `status` for native +owned live sentinel. Authored AArch64 auxiliary-header and numeric-unknown +carriers also pass through the configured decoder and both public adapters; +they are metadata fixtures, rather than captured AArch64 crashes. Their expected +names follow [pyelftools 0.33's enums](https://github.com/eliben/pyelftools/blob/v0.33/elftools/elf/enums.py#L431-L435) +and [machine-specific selection](https://github.com/eliben/pyelftools/blob/v0.33/elftools/elf/structs.py#L179-L184). +The [AArch64 ELF ABI](https://github.com/ARM-software/abi-aa/blob/2025Q1/aaelf64/aaelf64.rst#program-header) +defines their raw codes. An observed Node thread group must launch the exact +ownership `ps` inventory before it can read that live process's `stat` or `status` for native identity/UID facts. Only traced `CLONE_THREAD` members share this allowance; exec-replaced threads, ordinary children, debugger and decoder processes cannot use it. The purpose of stat/status reads within the same Node thread group diff --git a/docs/signal-recordings.md b/docs/signal-recordings.md index ab06cc2279..80a7a2c2ba 100644 --- a/docs/signal-recordings.md +++ b/docs/signal-recordings.md @@ -40,20 +40,24 @@ unrelated producer warnings do not change request status. Omitting `input_format` requests sigrok's autodetection. REA uses an immutable private copy whose filename retains the selected recording's final suffix, including a dotfile basename such as `.csv`. Filename-based format detection -receives that hint without exposing the -caller's directory to sigrok. The operand is an explicit relative path, so a -recording named `-` still selects the private file rather than sigrok stdin. -Omitting `annotations` -retains every annotation class. Both omitted selections are recorded as null. +receives that hint without exposing the caller's directory to sigrok. The +input operand is an explicit relative path within the private workspace, +so a recording named `-` is opened as a file rather than +[sigrok's stdin operand](https://github.com/sigrokproject/sigrok-cli/blob/sigrok-cli-0.7.2/input.c#L49-L75). +Omitting `annotations` retains every annotation class. Both omitted selections +are recorded as null. If sigrok reports an explicitly selected input format as unknown, REA returns `invalid_request` for `input_format`, retains the producer diagnostic, and -advises correcting the format or omitting it for autodetection. An explicit -`Unknown keyword` refusal naming a caller-selected format option also returns -`invalid_request` for `input_format`, even when sigrok exits zero and produces -fallback output. Output and diagnostics remain in `captured_output`; unrelated -warnings remain diagnostics. A completed nonzero `--version` probe is a -configuration refusal with its captured output and the selected sigrok/prlimit -configuration guidance. See the +advises correcting the format or omitting it for autodetection. The +[tagged option parser](https://github.com/sigrokproject/sigrok-cli/blob/sigrok-cli-0.7.2/parsers.c#L387-L404) +warns when an option key is unknown. When that diagnostic's key exactly matches +a selected `input_format` option, REA returns `invalid_request`, even on exit zero. +Fallback output and the diagnostic remain in `captured_output`; unrelated +option warnings stay visible without rejecting the request. Matching keeps +the complete diagnostic caption, including an LF inside a selected key; +embedded warning substrings do not establish a refusal. A completed nonzero +`--version` probe is a configuration refusal with its captured output and the +selected sigrok/prlimit configuration guidance. See the [upstream manual](https://sigrok.org/wiki/Sigrok-cli) for formats, decoder stacking and annotation selectors. No arbitrary command flags are accepted. diff --git a/docs/web-network-captures.md b/docs/web-network-captures.md index c9b50c00b7..c00818fd2d 100644 --- a/docs/web-network-captures.md +++ b/docs/web-network-captures.md @@ -171,7 +171,24 @@ fully qualified message type. All imports must be included. REA never fetches schemas or executes the selected application. Descriptor identity and parser version remain in the body facts. Scalars, repeated numeric packing and nested messages are interpreted alongside the unchanged wire facts; disagreements -remain explicit. Map semantics, extensions and required-field validation are +remain explicit. Descriptor admission rejects duplicate field numbers within +each message, including nested messages and oneof fields, before schema +annotation. This does not reject repeated occurrences in captured wire bytes +or reuse of a number in a different message. Fully qualified message, enum, +service and extension names must also identify one declaration across the +descriptor set, including nested and synthetic map-entry messages. Different +packages or enclosing messages may reuse a local type name. Schema-admission +failures retain the partial capture Evidence and its original artifact identity. +REA also requires an unambiguous descriptor file identity. Repeated exact file +names are coalesced only when protobuf-es equality confirms matching field +presence, declared values and retained opaque unknown/custom-option fields. +Opaque fields must match in their retained order and bytes; REA does not +interpret them. Conflicting definitions are refused rather than selecting a +file by its position. This is REA's supplied-schema admission policy, separate +from protobuf wire validity and the [first-occurrence precedence in protoc v32.1](https://github.com/protocolbuffers/protobuf/blob/v32.1/src/google/protobuf/compiler/command_line_interface.cc#L1402). +The descriptor-set byte count and SHA-256 continue to identify the complete +original input, including repeated file facts. +Map semantics, extensions and required-field validation are unsupported. A supplied schema does not establish the sender's actual schema. An invalid nested interpretation records `schema_status: "invalid-message"` and `schema_error` locally; later outer fields remain available. Valid string diff --git a/experiments/analysis-efficiency/native-export-costs.py b/experiments/analysis-efficiency/native-export-costs.py new file mode 100644 index 0000000000..8b6111e0cd --- /dev/null +++ b/experiments/analysis-efficiency/native-export-costs.py @@ -0,0 +1,115 @@ +"""Check equivalent export questions and count explicitly projected tokenizer costs.""" +import argparse +import hashlib +import json +from pathlib import Path +import statistics + +import tiktoken + + +def compact(value): + return json.dumps(value, ensure_ascii=False, separators=(",", ":"), allow_nan=False) + + +def inspect(path, encoding): + report = json.loads((path / "report.json").read_text()) + catalog = json.loads((path / "catalog.json").read_text()) + transcript = json.loads((path / "transcript.json").read_text()) + scope = next(json.loads(line) for line in reversed(path.with_suffix(".log").read_text().splitlines()) + if line.startswith('{"resource_scope":')) + observed = scope.get("payload_resource_observation") or {} + events = observed.get("resource_events", {}) + assert report["status"] == "passed" and report.get("runtime_identity_sha256") + assert not report["cleanup_failures"] and not report["receipt_failures"] + assert scope["abort"] is None and scope.get("payload_resource_observation_verified") is True + assert observed.get("payload_exit_code") == 0 and observed.get("payload_cleanup_verified") is True + assert all(events.get("memory.events", {}).get(key) == 0 for key in ["high", "max", "oom", "oom_kill"]) + assert events.get("pids.events", {}).get("max") == 0 + assert len(catalog["tools"]) == 220 + count = lambda text: len(encoding.encode(text, disallowed_special=())) + directory = [{key: tool[key] for key in ["name", "description", "inputSchema"]} for tool in catalog["tools"]] + question = (f"For {report['artifact']['path']}, report the export count and every version/alias of " + f"{', '.join(dict.fromkeys(row['name'] for row in report['answer']['rows']))}, including addresses, " + "sizes, types, binding and symbol-version facts. Check that __rea_export_oracle_missing__ " + "has no recorded match, preserve source coverage and export the complete Evidence. " + "Report static declarations without claiming runtime execution.") + arguments = sum(count(compact({"name": event["name"], "arguments": event["input"]})) for event in transcript) + replies = sum(count(block["text"]) for event in transcript for block in event["reply"]["content"] if block["type"] == "text") + inputs = count(compact(directory)) + raw_catalog = count(compact(catalog)) + task_directory = [tool for tool in directory if tool["name"] in ["inspect_native_exports", "inspect_analysis_view", "export_evidence_bundle"]] + return {"case": path.name, "lane": report["lane"], "artifact": report["artifact"], + "answer": report["answer"], "parent_evidence_id": report["parent_evidence_id"], + "source_report_sha256": report["source_report_sha256"], + "runtime_identity_sha256": report["runtime_identity_sha256"], + "catalog_sha256": report["catalog_sha256"], "catalog_tools": len(catalog["tools"]), + "buffer_bytes": report["buffer_bytes"], "calls": report["calls"], "phases": report["phases"], + "wall_seconds": scope["elapsed_seconds"], "cpu_seconds": observed["cpu_stat"]["usage_usec"] / 1e6, + "kernel_family_peak_bytes": observed["memory_peak_bytes"], + "events_at_payload_exit": events, "payload_cleanup_verified": True, + "final_whole_scope_events": scope.get("resource_events_final"), + "token_proxy": {"question": count(question), "input_catalog_once": inputs, "arguments": arguments, + "text_replies": replies, "combined": count(question) + inputs + arguments + replies, + "only_three_task_tools": count(question) + count(compact(task_directory)) + arguments + replies, + "complete_transport_catalog": raw_catalog}, + "receipt_sha256": {file.name: hashlib.sha256(file.read_bytes()).hexdigest() for file in + [path / "report.json", path / "catalog.json", path / "transcript.json", path / "evidence.json", path.with_suffix(".log")]}} + + +def summarize(rows): + result = {} + for artifact in sorted({row["artifact"]["sha256"] for row in rows}): + selected = [row for row in rows if row["artifact"]["sha256"] == artifact] + assert len({row["parent_evidence_id"] for row in selected}) == 1 + assert len({row["source_report_sha256"] for row in selected}) == 1 + assert all(row["answer"] == selected[0]["answer"] for row in selected) + result[artifact] = {} + for lane in ["complete", "selected"]: + samples = [row for row in selected if row["lane"] == lane] + assert len(samples) == 3 + metrics = {key: [row[key] for row in samples] for key in ["wall_seconds", "cpu_seconds", "kernel_family_peak_bytes"]} + metrics["projected_tokens"] = [row["token_proxy"]["combined"] for row in samples] + metrics["mcp_call_count"] = [len(row["calls"]) for row in samples] + metrics["mcp_reply_bytes"] = [sum(call["reply_bytes"] for call in row["calls"]) for row in samples] + result[artifact][lane] = {metric: {"median": statistics.median(values), "minimum": min(values), "maximum": max(values)} for metric, values in metrics.items()} + return result + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--cohort", type=Path, required=True) + parser.add_argument("cases", type=Path, nargs="+") + args = parser.parse_args() + manifest = json.loads((args.cohort / "manifest.json").read_text()) + index = json.loads((args.cohort / "index.json").read_text()) + assert len(index) == 12 and all(case["exit"] == 0 and case["status"] == "passed" for case in index) + assert {path.resolve() for path in args.cases} == {(args.cohort / case["name"]).resolve() for case in index} + assert len(args.cases) == len(index) + for file, digest in manifest["driver_files"].items(): + assert hashlib.sha256(Path(file).read_bytes()).hexdigest() == digest, file + runtimes = {} + for case in index: + path = Path(case["runtime"]) / "runtime-identity.json" + data = path.read_bytes() + identity = json.loads(data) + assert identity["source_clean"] is True + runtimes[hashlib.sha256(data).hexdigest()] = { + "source_revision": identity["source_revision"], + "compiled_files": len(identity["compiled_files"]), + "entrypoints": len(identity["entrypoints"]), + } + encoding = tiktoken.get_encoding("cl100k_base") + rows = [inspect(path, encoding) for path in args.cases] + assert all(row["runtime_identity_sha256"] in runtimes for row in rows) + receipt = {"tiktoken": tiktoken.__version__, "encoding": encoding.name, "actual_model_usage": None, + "cohort": manifest, "commands": index, "runtime_identities": runtimes, + "projection": "Scripted SDK tasks, not an actual model run. The estimate includes a fixed task description, every discovered tool name/description/input schema once, every tool argument and text reply, including Evidence export. Complete raw SDK directory token costs and a three-tool-only alternative are reported separately; output schemas are not included in the primary model-context projection. Host injection, history replay, inference quality and billable usage are unmeasured.", + "resources": "CPU quota 100%; launcher binds clients/servers to CPUs 0-1 (the outer observer starts on CPU15). The guard receipt affinity is initial placement, not child affinity; a live cohort observation records the override. 3 GiB early stop/4 GiB hard/no swap. Fresh client/server/HOME per task, Node compile cache disabled, OS caches unflushed. Later retained pages are warm queries; no separate warm-server cohort. Whole-family costs include client/oracle comparisons, source hashing, runtime binding, serialization and receipts. Tokenizer processing itself is outside these measured tasks. Verified counters are at payload exit while the observer remains alive; unavailable final whole-scope counters remain null.", + "summary": summarize(rows), "recordings": rows} + args.output.write_text(json.dumps(receipt, ensure_ascii=False, indent=2) + "\n") + + +if __name__ == "__main__": + main() diff --git a/experiments/analysis-efficiency/native-export-oracle.py b/experiments/analysis-efficiency/native-export-oracle.py new file mode 100644 index 0000000000..b0213e15d6 --- /dev/null +++ b/experiments/analysis-efficiency/native-export-oracle.py @@ -0,0 +1,48 @@ +"""Independent readelf symbol facts and directly mapped ELF64 version indices.""" +import hashlib +import json +from pathlib import Path +import re +import struct +import subprocess +import sys + +path = Path(sys.argv[1]).resolve() +selected_names = sys.argv[2:] +source = path.read_bytes() +assert source[:6] == b'\x7fELF\x02\x01', 'This experiment oracle only covers ELF64 little-endian' +section_offset = struct.unpack_from(' 0 +sections = [struct.unpack_from('