mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
Merge origin/main into fix/ib-object-limit-reasons
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
8c723269cb
224 files changed
+14322
-1115
No files matched your search
@@ -2,7 +2,7 @@
|
||||
name: reverse-engineer-anything
|
||||
description: Reverse engineer native, managed, Electron/JavaScript, packaged, firmware, and browser targets with REA. Use shipped-artifact or requested runtime evidence to explain features, compare versions, decompile code, or guide a reconstruction. Skip REA for ordinary source-repository architecture analysis.
|
||||
metadata:
|
||||
version: "43"
|
||||
version: "44"
|
||||
---
|
||||
|
||||
# REA
|
||||
@@ -30,6 +30,8 @@ unavailable, read [connection and recovery](references/connection-and-recovery.m
|
||||
Diagnose the observed blocker, continue useful supported work, and resume the
|
||||
original task after repair. Client approval, shell PATH, registration and engine
|
||||
availability have different remedies; avoid repeating setup for all of them.
|
||||
If the host times out without a REA result, do not repeat the unchanged call or
|
||||
assume work stopped. Read the recovery guide's deadline distinction before retrying.
|
||||
|
||||
1. Diagnose without changing REA or client configuration. `npx` may download
|
||||
the package and write to npm's cache. For Codex, run
|
||||
@@ -169,6 +171,15 @@ explicit path or endpoint and do not need it.
|
||||
absent metadata does not prove a binary is not Go, and unparsed text records
|
||||
remain explicit. See the
|
||||
[Go binary guide](https://github.com/morluto/rea/blob/main/docs/go-binaries.md).
|
||||
- Bundled GGUF or safetensors model declarations: `inspect_model_artifact`
|
||||
with the explicit local file. Default header hashing leaves full-file identity
|
||||
unknown; `hash: "full"` additionally reads all weights for SHA-256. MCP returns
|
||||
retained summaries; optional `view` selects one page in the initial call. Use
|
||||
`inspect_analysis_view` kind `model-artifact` for
|
||||
batched metadata keys, tensor names or type totals, selected fields and pages.
|
||||
Preserve header-scoped indices and array preview omissions; do not infer model
|
||||
behavior from declared architecture or tensor types. See the
|
||||
[model file guide](https://github.com/morluto/rea/blob/main/docs/model-artifacts.md).
|
||||
- Unity IL2CPP names, member owners and string literals from `global-metadata.dat`:
|
||||
`inspect_il2cpp_metadata` with the explicit local file; MCP defaults to compact
|
||||
counts. Query sections, images, types, methods, fields, properties, events,
|
||||
|
||||
@@ -34,6 +34,47 @@ answers part of the question, and name the unavailable evidence separately.
|
||||
Do not substitute another target. Label observations from ordinary shell tools
|
||||
as such; only actual REA output supplies REA Evidence.
|
||||
|
||||
## Distinguish client and server deadlines
|
||||
|
||||
A host message such as `timed out awaiting tools/call` with no REA result means
|
||||
the client's deadline expired before a result arrived. It does not establish
|
||||
that REA finished, canceled successfully, or released all resources. Do not
|
||||
repeat the unchanged analysis. Inspect available session/provider state and
|
||||
retained Evidence only when useful; an idle `binary_session` is not proof that
|
||||
every target-free operation stopped. Never invent a missing Evidence reference.
|
||||
|
||||
Allow the host tool deadline to cover execution, cancellation/cleanup, and
|
||||
response delivery. In Codex, the setting is
|
||||
`mcp_servers.<server-id>.tool_timeout_sec` in **seconds**, not
|
||||
`startup_timeout_sec`. Select a value for the actual operation budget and
|
||||
headroom, not a universal timeout. See the
|
||||
[official MCP configuration reference](https://learn.chatgpt.com/docs/extend/mcp?surface=cli).
|
||||
Apply configuration changes only within the authorized scope, preserving
|
||||
unrelated entries and backups; reconnect/restart as required by the host.
|
||||
When configuration is not editable, report the setting needed or use an
|
||||
authorized CLI fallback. Do not silently broaden permissions or switch targets.
|
||||
|
||||
A returned REA `provider_timeout` instead means the **server/provider** reached
|
||||
its deadline. Follow its `remediation.action`, reuse accessible partial Evidence,
|
||||
and resolve reported incomplete cleanup before retrying. Raising only the
|
||||
client deadline does not change this server budget. For static JavaScript,
|
||||
`analysis_timeout_ms` (CLI `rea analyze-javascript-application <path>
|
||||
--analysis-timeout-ms <milliseconds>`) controls each source's
|
||||
startup, analysis and transfer deadline in **milliseconds**. Increase that
|
||||
budget or reduce semantic work / select exact inventoried `source_paths` when
|
||||
the task permits. Omit `source_paths` for all sources; `[]` is invalid.
|
||||
`detail: "summary"` reduces delivery, not analysis work. A per-source budget
|
||||
does not bound the elapsed time for an entire multi-source application.
|
||||
Partial-Evidence inspection can avoid recomputation; an unavailable reader is
|
||||
not itself a reason to block a corrected static-analysis retry.
|
||||
The shorthand `rea analyze <path>` does not expose this timeout flag; use the
|
||||
explicit JavaScript command when changing that budget.
|
||||
|
||||
For invalid arguments, use the connected tool's current input schema, or the
|
||||
installed CLI's command help. Refresh the catalog if stale; do not guess fields
|
||||
from an older skill or return the entire catalog to repair one field. Correct
|
||||
all reported fields before retrying.
|
||||
|
||||
## Repair registration when needed
|
||||
|
||||
For Codex:
|
||||
|
||||
@@ -18,7 +18,8 @@ only selected files. The result and summary record `source_selection`; files
|
||||
outside scope and their relationships remain unknown, so partial application
|
||||
coverage does not mean the selected source failed. Use repeated `--source-path`
|
||||
on either static application CLI route. Omit the selection when the question
|
||||
requires complete application analysis. `detail: "summary"` reduces delivery
|
||||
requires complete application analysis; `source_paths: []` is invalid, not an
|
||||
unfiltered request. `detail: "summary"` reduces delivery
|
||||
and does not by itself reduce analysis work.
|
||||
|
||||
Exact `#alias` specifiers resolve through the importer's
|
||||
@@ -35,7 +36,9 @@ the complete application Evidence inline when it fits.
|
||||
When the connected schema advertises `max_heap_mb` and `analysis_timeout_ms`,
|
||||
these select the isolated source analyzer's old-space heap and per-source
|
||||
deadline. Defaults are 1024 MiB and 300000 ms. They do not change the MCP
|
||||
process heap. A Bun-hosted REA does not enforce the heap choice; on Windows it
|
||||
process heap or the client's tool deadline. For a host timeout without REA
|
||||
output, use the [deadline recovery guide](connection-and-recovery.md#distinguish-client-and-server-deadlines)
|
||||
instead of raising the server budget. A Bun-hosted REA does not enforce the heap choice; on Windows it
|
||||
instead limits the analyzer's committed memory to that choice plus a 512 MiB
|
||||
runtime reserve.
|
||||
Resource-limited analyses preserve completed static facts and
|
||||
|
||||
@@ -38,6 +38,16 @@ unparsed module-text line. A missing record is an unknown, not proof of a
|
||||
non-Go binary; observed metadata does not authenticate its claims. Function,
|
||||
source and type recovery remain a separate native-analysis question.
|
||||
|
||||
## Model files
|
||||
|
||||
Use `inspect_model_artifact` (`inspect-model-artifact PATH`) for local GGUF v2/v3
|
||||
and safetensors declarations without a model runtime. Header-only mode does not
|
||||
read weights or establish full-file identity. `hash: "full"` explicitly streams
|
||||
all bytes. Retained `model-artifact` views page `metadata`, `tensors` and derived
|
||||
`types`, with exact batch filters and selected fields. Arrays retain typed counts
|
||||
and eight-element previews; omitted values, unknown types and runtime behavior
|
||||
stay explicit. See [model files](https://github.com/morluto/rea/blob/main/docs/model-artifacts.md).
|
||||
|
||||
## IL2CPP metadata
|
||||
|
||||
Use `inspect_il2cpp_metadata` for an explicit local Unity `global-metadata.dat`
|
||||
|
||||
@@ -180,6 +180,7 @@ Additional tools and host support depend on the target:
|
||||
| Native binaries | Pseudocode, assembly, strings, symbols, calls and references | Hopper, Ghidra or IDA; [native analysis](docs/native-investigation.md) |
|
||||
| Offline ELF layout | Sections, segments, original symbols/relocations and static mitigation candidates | Caller-supplied pwntools on Linux x64; [binary diagnostics](docs/binary-diagnostics.md) |
|
||||
| Go binaries | Embedded compiler version, modules, replacements and build settings | Built-in static ELF/PE/thin Mach-O inspection; no Go installation required |
|
||||
| Model artifacts | GGUF / safetensors metadata, tensor declarations and derived totals | Built-in header inspection; optional full-file hash; [model files](docs/model-artifacts.md) |
|
||||
| IL2CPP metadata | Unity global-metadata.dat assembly images, type and method names and index ranges | Built-in static decoding of versions 24, 27, 29, 31 and 39; no Unity required |
|
||||
| Godot PCK | Archive inventory and extraction, original header/directory facts and selected content identities | Built-in static formats 2, 3 and 4; no Godot required |
|
||||
| Rust binaries | Embedded cargo-auditable crate names, versions, sources and dependency edges | Built-in static ELF/PE/thin Mach-O inspection; [Rust binaries](docs/rust-binaries.md) |
|
||||
|
||||
@@ -30,6 +30,7 @@ export default defineConfig({
|
||||
{ text: "Native binaries", link: "/native-investigation" },
|
||||
{ text: "Go build metadata", link: "/go-binaries" },
|
||||
{ text: "IL2CPP metadata", link: "/il2cpp-metadata" },
|
||||
{ text: "Model artifacts", link: "/model-artifacts" },
|
||||
{ text: "Rust dependencies", link: "/rust-binaries" },
|
||||
{
|
||||
text: "JavaScript and Electron",
|
||||
|
||||
@@ -201,6 +201,11 @@ rea inspect-web-page http://127.0.0.1:9222 TARGET_ID \
|
||||
--json
|
||||
```
|
||||
|
||||
Passive observation honors the requested duration using cancellable waits within
|
||||
the runtime's timer range. Long windows stay pending across successive waits;
|
||||
caller cancellation, target termination or leaving the allowed origins ends
|
||||
collection early. `window_elapsed` identifies completion of the requested wait.
|
||||
|
||||
Static bundle analysis and a user-driven observation window are separate operations:
|
||||
|
||||
```bash
|
||||
|
||||
+13
@@ -103,6 +103,19 @@ Formats 2, 3 and 4 retain header, directory and integrity facts. Compact deliver
|
||||
keeps complete Evidence in the requested bundle. See [Godot PCK](godot-pck.md)
|
||||
for embedded packs, batch queries and unknown encrypted coverage.
|
||||
|
||||
Inspect local model declarations without loading weights:
|
||||
|
||||
```bash
|
||||
rea inspect-model-artifact /absolute/path/model.gguf --json
|
||||
rea inspect-model-artifact /absolute/path/model.safetensors --hash full --json
|
||||
```
|
||||
|
||||
Default hashing reads only headers and leaves full-file identity unknown.
|
||||
`--hash full` streams the complete file for SHA-256. Both return typed metadata,
|
||||
original tensor declarations and derived totals. MCP summaries retain complete
|
||||
Evidence for filtered metadata, tensor and type-total pages; CLI compact delivery
|
||||
uses `--detail summary --evidence_output FILE`. See [model files](model-artifacts.md).
|
||||
|
||||
Inspect Unity IL2CPP metadata names without Unity or a disassembler:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -457,13 +457,22 @@ disposable HOME and CODEX_HOME under `~/.cache/rea-agent-evaluations/`. Set
|
||||
`REA_AGENT_EVAL_ROOT` to choose another parent directory outside the OS temporary
|
||||
directory: current Codex refuses helper aliases under `/tmp`, preventing shell
|
||||
commands and skill reads even if MCP works. Each owned run directory is removed
|
||||
after verification unless fixture retention is requested. Cleanup attempts the
|
||||
after verification unless fixture retention is requested. Commands share the
|
||||
owned-process supervisor: native ownership preparation has its own bounded
|
||||
deadline before each short command budget, and timeouts join bounded process-tree
|
||||
and output cleanup before returning. If cleanup remains incomplete, the lane retries its retained
|
||||
owner and preserves the run directory (including disposable auth) rather than
|
||||
removing files still in use; the error retains ownership and recovery details.
|
||||
Command stderr is limited to 32 KiB and combined version output to 8 KiB;
|
||||
exceeding these diagnostic budgets fails explicitly. Scenario JSON events stream
|
||||
to the evaluator without retaining a second raw stdout copy. Cleanup attempts the
|
||||
remaining run tree even if auth removal fails, and reports unresolved cleanup
|
||||
together with the original verification failure. The lane
|
||||
installs the packaged skill and MCP registration through public `rea setup`,
|
||||
rather than copying a skill into a project that also inherits the caller's
|
||||
skills and configuration. Only authentication is copied, when present, and it
|
||||
is removed even with `REA_AGENT_EVAL_KEEP_FIXTURES=true`. No user configuration,
|
||||
is removed after confirmed process cleanup even with
|
||||
`REA_AGENT_EVAL_KEEP_FIXTURES=true`. No user configuration,
|
||||
rules, plugins or history is copied; shell snapshots and additional agents are
|
||||
disabled. Existing API-key environment authentication remains available.
|
||||
The native missing-provider scenario preapproves only `open_binary` and
|
||||
|
||||
@@ -19,6 +19,56 @@ requires an absolute filesystem path and preserves that exact selection:
|
||||
}
|
||||
```
|
||||
|
||||
For build identity and counts, select `detail: "summary"`. MCP retains the same
|
||||
complete Evidence and returns `normalized_result.parent_evidence_id`. Read
|
||||
selected dependency versions without reopening the file:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "inspect_analysis_view",
|
||||
"arguments": {
|
||||
"source": {
|
||||
"kind": "retained-evidence",
|
||||
"evidence_id": "ev_<parent digest>"
|
||||
},
|
||||
"view": {
|
||||
"kind": "go",
|
||||
"collection": "dependencies",
|
||||
"paths": ["golang.org/x/crypto"],
|
||||
"fields": ["path", "version", "replacement"],
|
||||
"offset": 0,
|
||||
"limit": 10
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Go views support `dependencies` (exact `paths`) and `settings` (exact `keys`),
|
||||
optional original `indices`, field selection, and pagination. Filters intersect;
|
||||
empty filter arrays select no rows and `fields: []` returns only row identity.
|
||||
Each row keeps its original `index`, scoped to the parent Evidence and collection;
|
||||
duplicate module paths and setting keys keep distinct indices. Page offsets
|
||||
count matching records. Exhausting retained rows does not establish complete
|
||||
runtime dependencies. Original bytes, unknown lines and all complete metadata
|
||||
remain in the parent and can be exported as an Evidence bundle.
|
||||
|
||||
One-shot CLI summaries save complete Evidence before returning the projection:
|
||||
|
||||
```bash
|
||||
rea inspect-go-binary ./application --detail summary --evidence_output ./go-evidence.json --json
|
||||
```
|
||||
|
||||
`--evidence_output` is required for CLI summary delivery and never overwrites an
|
||||
existing file. The default CLI/MCP reply remains complete.
|
||||
|
||||
[Measured workflows](../experiments/analysis-efficiency/go-build-views.md) include
|
||||
catalog repetition and extra calls. Identity/count questions benefit from a
|
||||
single summary. In the tested Codex full-catalog configuration, a compound
|
||||
question about identity and one dependency uses fewer tokens in captured request JSON with
|
||||
one complete inspection than with a summary followed by a lookup. The complete
|
||||
reply remains useful for that initial question; retained views support later
|
||||
selected reads. Token figures are serialization estimates, not model usage.
|
||||
|
||||
## Observations
|
||||
|
||||
The inline Evidence result contains the artifact path, SHA-256, byte length,
|
||||
|
||||
+21
-3
@@ -95,9 +95,10 @@ optional bounded `recovery` next step. `recovery.kind=inspect_input_contract`
|
||||
names the rejected operation: re-read that operation's advertised input schema
|
||||
(call `tools/list` if the catalog may be stale), correct the listed fields, and
|
||||
do not repeat the unchanged call. `recovery.kind=inspect_timeout_state` names
|
||||
the operation and the REA server/provider deadline; inspect session/provider
|
||||
state before retrying, and raise the MCP client's tool deadline only when the
|
||||
client ended the call before REA returned this response. These hints never
|
||||
the operation and the REA server/provider deadline; inspect partial Evidence
|
||||
and reported cleanup state, then follow producer-specific remediation or reduce
|
||||
supported work. Generic errors do not assume adjustable timeout controls. Raising
|
||||
only the MCP client's deadline cannot extend this budget. These hints never
|
||||
include the complete catalog or caller arguments.
|
||||
Failures omit `structuredContent`, since each advertised `outputSchema`
|
||||
describes successful results. Successful replies retain their schema-validated
|
||||
@@ -239,6 +240,23 @@ delivery for responses that fit that buffer and Node's single-string limit.
|
||||
It does not change the client's buffer, analysis coverage, or retained content.
|
||||
Ordinary responses keep their existing complete result contract.
|
||||
|
||||
### Error recovery and deadline ownership
|
||||
|
||||
Initialization instructions and the static JavaScript tool description teach
|
||||
recovery before a host can discard a tool response. Invalid input calls should be corrected from the connected schema
|
||||
(refresh `tools/list` only if stale), not retried unchanged. REA's returned
|
||||
`provider_timeout` describes its server/provider deadline; provider-authored
|
||||
remediation names the applicable controls and preserves partial Evidence and
|
||||
cleanup uncertainty.
|
||||
|
||||
A host timeout without a REA result is different: execution and cleanup remain
|
||||
unknown. Increasing an advertised `analysis_timeout_ms` cannot fix a shorter
|
||||
client deadline. Configure the client's tool timeout with execution, cleanup
|
||||
and delivery headroom, or report that change is needed when not authorized.
|
||||
Session status does not establish that all target-free work stopped. Detailed
|
||||
host settings and bounded retry guidance live in the shipped skill's
|
||||
[recovery reference](https://github.com/morluto/rea/blob/main/.agents/skills/reverse-engineer-anything/references/connection-and-recovery.md#distinguish-client-and-server-deadlines).
|
||||
|
||||
### Untrusted target content
|
||||
|
||||
Strings, symbols, decompiled code and pseudocode, page text, script bodies,
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
# Model file declarations
|
||||
|
||||
Use `inspect_model_artifact` (CLI: `inspect-model-artifact`) to inspect a local
|
||||
GGUF v2/v3 or safetensors file. Detection uses content, including GGUF byte
|
||||
order, rather than its extension. REA does not load a model, interpret weights,
|
||||
run inference, import Python, open external-data references or use the network.
|
||||
|
||||
```bash
|
||||
rea inspect-model-artifact /absolute/path/model.gguf --json
|
||||
rea inspect-model-artifact /absolute/path/model.safetensors --hash full --json
|
||||
```
|
||||
|
||||
The CLI defaults to complete Evidence. For compact delivery with portable access
|
||||
to every retained observation:
|
||||
|
||||
```bash
|
||||
rea inspect-model-artifact /absolute/path/model.gguf \
|
||||
--detail summary --evidence_output /absolute/path/model-evidence.json --json
|
||||
```
|
||||
|
||||
## Identity and reads
|
||||
|
||||
The default `hash: "header"` reads only the metadata and tensor directory. The
|
||||
result records the exact selected path, file size, header length, header SHA-256,
|
||||
bytes read and read scope. GGUF alignment padding is outside this prefix.
|
||||
`artifact.sha256` and the Evidence subject are null: a header digest is not a
|
||||
commitment to the weights. Different files can have identical headers and sizes.
|
||||
|
||||
`hash: "full"` additionally streams the remaining file bytes through SHA-256.
|
||||
It returns a full-file digest and Evidence subject, while retaining the same
|
||||
header digest. It uses bounded buffers and does not interpret tensor data, but
|
||||
it does read the whole file. Cost therefore grows with file size. Both modes
|
||||
check descriptor and selected-path identity and metadata before completion;
|
||||
this detects observed replacement or modification, not adversarial modification
|
||||
that defeats filesystem metadata checks.
|
||||
|
||||
## Facts and selected queries
|
||||
|
||||
GGUF reports version, byte order, alignment, typed metadata, and each tensor's
|
||||
name, original type ID, dimensions, relative data offset, file offset and derived
|
||||
byte length. Alignment accepts every positive UINT32 multiple of eight, including
|
||||
non-powers of two. Metadata keys use ASCII lower-snake-case hierarchy components;
|
||||
standard numeric components such as `general.base_model.0.name` are preserved.
|
||||
Scalar metadata is complete. Arrays have declared element types,
|
||||
counts and at most eight preview elements per array, with an explicit
|
||||
`preview_complete` flag. This includes tokenizer vocabularies: omitted values
|
||||
are not silently represented as a complete array. Strings are UTF-8; 64-bit
|
||||
integer metadata and all shape dimensions use decimal strings without rounding.
|
||||
Nonfinite floating metadata uses `NaN`, `+Infinity` or `-Infinity` with its
|
||||
declared float type; negative zero is `"-0"`. Unknown tensor types retain their numeric IDs and null byte
|
||||
lengths; affected totals and storage validation remain unknown.
|
||||
|
||||
Safetensors reports the header extent, exact string `__metadata__` values, dtype,
|
||||
shape and declared data ranges. JSON duplicate keys, invalid UTF-8, out-of-file
|
||||
ranges, overlaps, gaps, trailing unindexed data and known dtype/shape byte-length
|
||||
contradictions are rejected with a typed `file_format_rejected` constraint (public code `target_unavailable`, category `unsupported_target`). Empty
|
||||
tensors and scalar shapes are supported; packed sub-byte types require whole
|
||||
byte storage. Unknown dtypes retain declared ranges and an explicit limitation
|
||||
that shape-to-storage validation is unavailable. Each tensor records
|
||||
`storage_validation: "consistent" | "unknown"`; `coverage.storage_sizes` remains
|
||||
partial when that consistency cannot be checked. Tensor filters accept these
|
||||
`storage_validation` states independently of dtype or name.
|
||||
|
||||
Parameter counts and per-type totals are **derived from declarations**, not
|
||||
measurements of effective parameters or inference behavior. Shared or repeated
|
||||
weights are not deduplicated. GGUF dimensions retain their original fastest-first
|
||||
order; safetensors shapes retain JSON order. Safetensors tensor rows use name
|
||||
order; GGUF rows use directory order. An index identifies a declaration within
|
||||
its collection and header digest, not tensor contents or a cross-version match.
|
||||
GGUF supports zero extents and scalar shapes, with at most four dimensions;
|
||||
known quantized types still require complete blocks in the first dimension.
|
||||
|
||||
MCP defaults to a compact summary with counts, selected short headline metadata,
|
||||
chat-template presence and derived totals. The full type histogram and all rows
|
||||
remain in complete Evidence. Headline metadata uses short keys/types and scalar
|
||||
values; long or compound values remain available through explicit metadata
|
||||
queries. Use the returned `parent_evidence_id`:
|
||||
|
||||
```json
|
||||
{
|
||||
"source": {
|
||||
"kind": "retained-evidence",
|
||||
"evidence_id": "<parent_evidence_id>"
|
||||
},
|
||||
"view": {
|
||||
"kind": "model-artifact",
|
||||
"collection": "tensors",
|
||||
"names": ["token_embd.weight", "output.weight"],
|
||||
"fields": ["name", "type", "shape", "file_offset", "byte_length"],
|
||||
"offset": 0,
|
||||
"limit": 32
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To answer a known question in the initial inspection, pass the same selection
|
||||
without `kind` as `inspect_model_artifact`'s optional `view`, for example
|
||||
`{"path":"/absolute/path/model.gguf","view":{"collection":"tensors","names":["token_embd.weight"],"fields":["type","shape","file_offset"]}}`.
|
||||
This returns one selected page with compact model context and retains the full
|
||||
source Evidence, avoiding another tool call. `view` requires summary delivery;
|
||||
complete detail and a selection are rejected as conflicting choices.
|
||||
CLI uses `--view '{"collection":"tensors","names":["token_embd.weight"]}'`
|
||||
with `--detail summary --evidence_output FILE`.
|
||||
|
||||
Call `inspect_analysis_view` with the historical query object above for further pages. `collection: "metadata"` accepts
|
||||
exact `keys`, indices, fields and paging; use it for `tokenizer.chat_template`
|
||||
or tokenizer array previews. `collection: "types"` pages derived storage and
|
||||
parameter totals and accepts exact `types`. Tensor pages also accept `types`.
|
||||
Filters intersect; omitted filters mean all recorded rows and empty arrays mean
|
||||
none. Every row retains its source-scoped index. Names, keys and type labels
|
||||
follow field selection; `fields: []` returns only indices. Omitted fields return
|
||||
all recorded columns. Compact warnings use the shared bounded view budget and
|
||||
link to complete parent Evidence when messages are omitted. Page offsets count filtered
|
||||
matches, while row indices retain their original scope. Each page carries source
|
||||
identity, header coverage, array omissions, unknowns and parent Evidence links.
|
||||
|
||||
Pages read historical retained facts without reopening the model. Complete
|
||||
Evidence can also be supplied inline or as a single authenticated Evidence JSON
|
||||
record through `source: {"kind": "evidence-file", "path": "/absolute/path/model-record.json"}`.
|
||||
CLI Evidence bundles contain a `records` array; import the bundle with
|
||||
`import_evidence_bundle` before using retained queries, or save one complete
|
||||
record as a standalone Evidence file. A bundle is not a single Evidence record.
|
||||
The session owns validated indexes until that Evidence is released; there is no
|
||||
live-file result cache or automatic freshness claim.
|
||||
Imported reports must retain positional indices and unique names, keys and
|
||||
type identities; malformed reports return a typed input error before indexing.
|
||||
|
||||
## Bounds and unsupported inputs
|
||||
|
||||
The reader admits one active inspection per provider. It enforces a 100,000,000
|
||||
byte header ceiling (safetensors counts its declared JSON bytes separately from
|
||||
the fixed eight-byte length prefix; GGUF counts the complete header) and a separate 32 MiB
|
||||
retained representation budget before retaining rows or expanding strings.
|
||||
Numeric products use exact integer arithmetic. GGUF follows its four-dimensional
|
||||
tensor layout and limits metadata array nesting to 32 frames. Resource refusals
|
||||
are typed, rather than returned as complete empty inventories. Cancellation
|
||||
joins descriptor finalization; failed cleanup remains owned for retry.
|
||||
|
||||
PyTorch pickle and ZIP model deserialization are unsupported; convert using a
|
||||
trusted external workflow and supply GGUF or safetensors. ONNX, TFLite, Core ML,
|
||||
model execution and conversion are outside this operation.
|
||||
|
||||
Format references: [GGUF specification](https://github.com/ggml-org/ggml/blob/da8e2a54b919557b816b1c2beb432ccb766f7ce0/docs/gguf.md),
|
||||
[ggml tensor layouts](https://github.com/ggml-org/ggml/blob/da8e2a54b919557b816b1c2beb432ccb766f7ce0/src/ggml-common.h),
|
||||
[safetensors format](https://github.com/huggingface/safetensors/tree/e246a2560645b7525f5775669ed816eb57c5bcc8#format).
|
||||
@@ -266,6 +266,7 @@ npx rea-agents@latest setup
|
||||
|
||||
- [التثبيت والإعداد](../installation.md): تسجيل الوكلاء، وتهيئة الموفّرين، والتحديثات، وإلغاء التثبيت.
|
||||
- [الجاهزية واستكشاف الأخطاء](../installation.md#check-readiness-for-your-task): تشخيص وكيل أو محرك تحليل محدد.
|
||||
- [GGUF / safetensors](../model-artifacts.md)
|
||||
- [CLI وEvidence](../cli.md): الأوامر، واختيار الموفّر، واللقطات، والاستيراد/التصدير، وحالات الخروج.
|
||||
- [عقود MCP](../mcp-contracts.md) و[تعليمات الوكلاء](../mcp-prompts.md): نتائج الأدوات، والجلسات، والاستقصاءات الموجّهة.
|
||||
- [كتالوج الأدوات](../mcp-contracts.md#generated-catalog): قائمة الأدوات والموفّرين وأوامر CLI المولّدة أثناء البناء.
|
||||
|
||||
@@ -264,6 +264,7 @@ Beginne mit den [praxisnahen Anleitungen](https://rea.tools/de/guides/) auf der
|
||||
|
||||
- [Installation und Einrichtung](../installation.md): Agentenregistrierung, Provider-Konfiguration, Updates und Deinstallation.
|
||||
- [Bereitschaft und Fehlerbehebung](../installation.md#check-readiness-for-your-task): Diagnose eines bestimmten Agenten oder einer Analyse-Engine.
|
||||
- [GGUF / safetensors](../model-artifacts.md)
|
||||
- [CLI und Evidence](../cli.md): Befehle, Provider-Auswahl, Snapshots, Import/Export und Exit-Status.
|
||||
- [MCP-Kontrakte](../mcp-contracts.md) und [Agenten-Prompts](../mcp-prompts.md): Werkzeugergebnisse, Sitzungen und geführte Untersuchungen.
|
||||
- [Werkzeugkatalog](../mcp-contracts.md#generated-catalog): Beim Build erzeugtes Inventar von Werkzeugen, Providern und CLI-Befehlen.
|
||||
|
||||
@@ -277,6 +277,7 @@ npx rea-agents@latest setup
|
||||
|
||||
- [Εγκατάσταση και ρύθμιση](../installation.md): καταχώριση agents, ρύθμιση παρόχων ανάλυσης, ενημερώσεις και απεγκατάσταση.
|
||||
- [Έλεγχος ετοιμότητας και αντιμετώπιση προβλημάτων](../installation.md#check-readiness-for-your-task): διάγνωση ενός agent ή μιας μηχανής ανάλυσης.
|
||||
- [GGUF / safetensors](../model-artifacts.md)
|
||||
- [CLI και Evidence](../cli.md): εντολές, επιλογή παρόχου, στιγμιότυπα, εισαγωγή/εξαγωγή και κωδικοί εξόδου.
|
||||
- [Προδιαγραφές MCP](../mcp-contracts.md) και [prompts για agents](../mcp-prompts.md): αποτελέσματα εργαλείων, συνεδρίες και καθοδηγούμενες διερευνήσεις.
|
||||
- [Κατάλογος εργαλείων](../mcp-contracts.md#generated-catalog): αυτόματα παραγόμενος κατάλογος εργαλείων, παρόχων και εντολών CLI.
|
||||
|
||||
@@ -264,6 +264,7 @@ Empieza con las [guías prácticas](https://rea.tools/es/guides/) del sitio web.
|
||||
|
||||
- [Instalación y configuración](../installation.md): registro de agentes, configuración de proveedores, actualizaciones y desinstalación.
|
||||
- [Preparación y resolución de problemas](../installation.md#check-readiness-for-your-task): diagnóstico de un agente o motor de análisis concreto.
|
||||
- [GGUF / safetensors](../model-artifacts.md)
|
||||
- [CLI y Evidence](../cli.md): comandos, selección de proveedores, instantáneas, importación/exportación y estados de salida.
|
||||
- [Contratos de MCP](../mcp-contracts.md) y [prompts para agentes](../mcp-prompts.md): resultados de herramientas, sesiones e investigaciones guiadas.
|
||||
- [Catálogo de herramientas](../mcp-contracts.md#generated-catalog): inventario de herramientas, proveedores y comandos de CLI generado durante la compilación.
|
||||
|
||||
@@ -249,6 +249,7 @@ npx rea-agents@latest setup
|
||||
|
||||
- [نصب و راهاندازی](../installation.md): ثبت ایجنت، پیکربندی ارائهدهنده، بهروزرسانی و حذف نصب.
|
||||
- [آمادگی و رفع اشکال](../installation.md#check-readiness-for-your-task): عیبیابی یک ایجنت یا موتور تحلیل.
|
||||
- [GGUF / safetensors](../model-artifacts.md)
|
||||
- [CLI و شواهد](../cli.md): دستورات، انتخاب ارائهدهنده، snapshotها، ورود/خروج داده و وضعیتهای خروج.
|
||||
- [قراردادهای MCP](../mcp-contracts.md) و [پرامپتهای ایجنت](../mcp-prompts.md): نتایج ابزارها، نشستها و بررسیهای هدایتشده.
|
||||
- [فهرست ابزارها](../mcp-contracts.md#generated-catalog): فهرست تولیدشده هنگام build از ابزارها، ارائهدهندگان و دستورات CLI.
|
||||
|
||||
@@ -268,6 +268,7 @@ Commencez par les [guides pas à pas](https://rea.tools/fr/guides/) du site web.
|
||||
|
||||
- [Installation et configuration](../installation.md) : enregistrement des agents, configuration des fournisseurs, mises à jour et désinstallation.
|
||||
- [Vérification et dépannage](../installation.md#check-readiness-for-your-task) : diagnostiquer un agent ou un moteur d'analyse donné.
|
||||
- [GGUF / safetensors](../model-artifacts.md)
|
||||
- [CLI et Evidence](../cli.md) : commandes, choix du fournisseur, snapshots, import/export et codes de sortie.
|
||||
- [Contrats MCP](../mcp-contracts.md) et [prompts pour agents](../mcp-prompts.md) : résultats des outils, sessions et investigations guidées.
|
||||
- [Catalogue des outils](../mcp-contracts.md#generated-catalog) : inventaire des outils, fournisseurs et commandes CLI, généré lors du build.
|
||||
|
||||
@@ -289,6 +289,7 @@ Untuk mengetahui opsi, prasyarat, dan kontrak hasil secara tepat:
|
||||
|
||||
- [Instalasi dan penyiapan](../installation.md): pendaftaran agen, konfigurasi penyedia, pembaruan, dan penghapusan instalasi.
|
||||
- [Kesiapan dan pemecahan masalah](../installation.md#check-readiness-for-your-task): mendiagnosis satu agen atau mesin analisis.
|
||||
- [GGUF / safetensors](../model-artifacts.md)
|
||||
- [CLI dan Bukti](../cli.md): perintah, pemilihan penyedia, snapshot, impor/ekspor, dan status keluar.
|
||||
- [Kontrak MCP](../mcp-contracts.md) dan [prompt agen](../mcp-prompts.md): hasil alat, sesi, dan investigasi terpandu.
|
||||
- [Katalog alat](../mcp-contracts.md#generated-catalog): inventaris alat, penyedia, dan perintah CLI yang dibuat secara otomatis.
|
||||
|
||||
@@ -268,6 +268,7 @@ Inizia dalle [guide pratiche](https://rea.tools/it/guides/) del sito web. Per op
|
||||
|
||||
- [Installazione e configurazione](../installation.md): registrazione agente, configurazione provider, aggiornamenti e disinstallazione.
|
||||
- [Pronto all'uso e risoluzione dei problemi](../installation.md#check-readiness-for-your-task): diagnostica un agente o un motore di analisi.
|
||||
- [GGUF / safetensors](../model-artifacts.md)
|
||||
- [CLI ed Evidence](../cli.md): comandi, selezione provider, snapshot, import/export e stati di uscita.
|
||||
- [Contratti MCP](../mcp-contracts.md) e [prompt agente](../mcp-prompts.md): risultati degli strumenti, sessioni e indagini guidate.
|
||||
- [Catalogo strumenti](../mcp-contracts.md#generated-catalog): inventario generato di strumenti, provider e comandi CLI.
|
||||
|
||||
@@ -266,6 +266,7 @@ npx rea-agents@latest setup
|
||||
|
||||
- [インストールとセットアップ](../installation.md):エージェントの登録、プロバイダーの設定、更新、アンインストール。
|
||||
- [準備状況の確認とトラブルシューティング](../installation.md#check-readiness-for-your-task):特定のエージェントや分析エンジンを診断。
|
||||
- [GGUF / safetensors](../model-artifacts.md)
|
||||
- [CLI と Evidence](../cli.md):コマンド、プロバイダーの選択、スナップショット、インポート/エクスポート、終了ステータス。
|
||||
- [MCP 契約](../mcp-contracts.md)と[エージェント向けプロンプト](../mcp-prompts.md):ツールの結果、セッション、ガイド付き調査。
|
||||
- [ツールカタログ](../mcp-contracts.md#generated-catalog):ビルド時に生成されるツール、プロバイダー、CLI コマンドの一覧。
|
||||
|
||||
@@ -279,6 +279,7 @@ npx rea-agents@latest setup
|
||||
|
||||
- [설치 및 설정](../installation.md): 에이전트 등록, 제공자 설정, 업데이트, 제거.
|
||||
- [준비 상태 확인 및 문제 해결](../installation.md#check-readiness-for-your-task): 특정 에이전트 또는 분석 엔진 진단.
|
||||
- [GGUF / safetensors](../model-artifacts.md)
|
||||
- [CLI 및 Evidence](../cli.md): 명령, 제공자 선택, 스냅샷, 가져오기/내보내기, 종료 상태.
|
||||
- [MCP 계약](../mcp-contracts.md) 및 [에이전트 프롬프트](../mcp-prompts.md): 도구 결과, 세션, 안내에 따른 조사.
|
||||
- [도구 카탈로그](../mcp-contracts.md#generated-catalog): 빌드 시 생성되는 도구, 제공자, CLI 명령 목록.
|
||||
|
||||
@@ -282,6 +282,7 @@ npx rea-agents@latest setup
|
||||
|
||||
- [Installation and setup](../installation.md): agent registration, provider configuration, updates, uninstall.
|
||||
- [Readiness and troubleshooting](../installation.md#check-readiness-for-your-task): ഒരു agent-ന്റെയോ analysis engine-ന്റെയോ പ്രശ്നങ്ങൾ കണ്ടെത്തുക.
|
||||
- [GGUF / safetensors](../model-artifacts.md)
|
||||
- [CLI and Evidence](../cli.md): commands, provider തിരഞ്ഞെടുക്കൽ, snapshots, import/export, exit statuses.
|
||||
- [MCP contracts](../mcp-contracts.md), [agent prompts](../mcp-prompts.md): tool results, sessions, guided investigations.
|
||||
- [Tool catalog](../mcp-contracts.md#generated-catalog): tools, providers, CLI commands എന്നിവയുടെ സ്വയമേവ സൃഷ്ടിച്ച പട്ടിക.
|
||||
|
||||
@@ -264,6 +264,7 @@ Zacznij od [przewodników z przykładami](https://rea.tools/pl/guides/) na stron
|
||||
|
||||
- [Instalacja i konfiguracja](../installation.md): rejestracja agentów, konfiguracja dostawców analizy, aktualizacje i odinstalowanie.
|
||||
- [Gotowość i rozwiązywanie problemów](../installation.md#check-readiness-for-your-task): diagnostyka jednego agenta lub silnika analizy.
|
||||
- [GGUF / safetensors](../model-artifacts.md)
|
||||
- [CLI i Evidence](../cli.md): polecenia, wybór dostawcy, migawki, import/eksport i kody zakończenia.
|
||||
- [Kontrakty MCP](../mcp-contracts.md) i [prompty dla agentów](../mcp-prompts.md): wyniki narzędzi, sesje i prowadzone analizy.
|
||||
- [Katalog narzędzi](../mcp-contracts.md#generated-catalog): generowany podczas kompilacji wykaz narzędzi, dostawców i poleceń CLI.
|
||||
|
||||
@@ -264,6 +264,7 @@ Comece pelos [guias práticos](https://rea.tools/pt-BR/guides/) do site. Para op
|
||||
|
||||
- [Instalação e configuração](../installation.md): registro de agentes, configuração de provedores, atualizações e desinstalação.
|
||||
- [Prontidão e solução de problemas](../installation.md#check-readiness-for-your-task): diagnóstico de um agente ou motor de análise específico.
|
||||
- [GGUF / safetensors](../model-artifacts.md)
|
||||
- [CLI e Evidence](../cli.md): comandos, seleção de provedores, snapshots, importação/exportação e status de saída.
|
||||
- [Contratos MCP](../mcp-contracts.md) e [prompts para agentes](../mcp-prompts.md): resultados das ferramentas, sessões e investigações guiadas.
|
||||
- [Catálogo de ferramentas](../mcp-contracts.md#generated-catalog): inventário de ferramentas, provedores e comandos da CLI gerado durante o build.
|
||||
|
||||
@@ -264,6 +264,7 @@ npx rea-agents@latest setup
|
||||
|
||||
- [Установка и настройка](../installation.md): регистрация агентов, настройка провайдеров, обновление и удаление.
|
||||
- [Готовность и устранение неполадок](../installation.md#check-readiness-for-your-task): диагностика конкретного агента или движка анализа.
|
||||
- [GGUF / safetensors](../model-artifacts.md)
|
||||
- [CLI и Evidence](../cli.md): команды, выбор провайдера, снимки, импорт/экспорт и коды завершения.
|
||||
- [Контракты MCP](../mcp-contracts.md) и [промпты агентов](../mcp-prompts.md): результаты инструментов, сеансы и исследования с подсказками.
|
||||
- [Каталог инструментов](../mcp-contracts.md#generated-catalog): создаваемый при сборке перечень инструментов, провайдеров и команд CLI.
|
||||
|
||||
@@ -264,6 +264,7 @@ npx rea-agents@latest setup
|
||||
|
||||
- [การติดตั้งและตั้งค่า](../installation.md): การลงทะเบียนเอเจนต์ การตั้งค่าผู้ให้บริการ การอัปเดต และการถอนการติดตั้ง
|
||||
- [ความพร้อมใช้งานและการแก้ไขปัญหา](../installation.md#check-readiness-for-your-task): ตรวจวินิจฉัยเอเจนต์หรือเอนจินวิเคราะห์หนึ่งตัว
|
||||
- [GGUF / safetensors](../model-artifacts.md)
|
||||
- [CLI และ Evidence](../cli.md): คำสั่ง การเลือกผู้ให้บริการ สแนปช็อต การนำเข้า/ส่งออก และรหัสการสิ้นสุด
|
||||
- [สัญญา MCP](../mcp-contracts.md) และ[พรอมป์ต์สำหรับเอเจนต์](../mcp-prompts.md): ผลลัพธ์เครื่องมือ เซสชัน และการตรวจสอบตามคำแนะนำ
|
||||
- [แค็ตตาล็อกเครื่องมือ](../mcp-contracts.md#generated-catalog): รายการเครื่องมือ ผู้ให้บริการ และคำสั่ง CLI ที่สร้างระหว่างการบิลด์
|
||||
|
||||
@@ -269,6 +269,7 @@ Web sitesindeki [uygulamalı rehberlerden](https://rea.tools/tr/guides/) başlay
|
||||
|
||||
- [Yükleme ve kurulum](../installation.md): ajan kaydı, sağlayıcı yapılandırması, güncellemeler ve kaldırma.
|
||||
- [Hazırlık ve sorun giderme](../installation.md#check-readiness-for-your-task): belirli bir ajanı veya analiz motorunu tanılama.
|
||||
- [GGUF / safetensors](../model-artifacts.md)
|
||||
- [CLI ve Evidence](../cli.md): komutlar, sağlayıcı seçimi, anlık görüntüler, içe/dışa aktarma ve çıkış durumları.
|
||||
- [MCP sözleşmeleri](../mcp-contracts.md) ve [ajan istemleri](../mcp-prompts.md): araç sonuçları, oturumlar ve yönlendirmeli araştırmalar.
|
||||
- [Araç kataloğu](../mcp-contracts.md#generated-catalog): derleme sırasında oluşturulan araç, sağlayıcı ve CLI komutu envanteri.
|
||||
|
||||
@@ -264,6 +264,7 @@ npx rea-agents@latest setup
|
||||
|
||||
- [Встановлення й налаштування](../installation.md): реєстрація агентів, конфігурація провайдерів, оновлення та видалення.
|
||||
- [Готовність та усунення неполадок](../installation.md#check-readiness-for-your-task): діагностика конкретного агента або рушія аналізу.
|
||||
- [GGUF / safetensors](../model-artifacts.md)
|
||||
- [CLI та Evidence](../cli.md): команди, вибір провайдера, знімки, імпорт/експорт і статуси завершення.
|
||||
- [Контракти MCP](../mcp-contracts.md) і [промпти агентів](../mcp-prompts.md): результати інструментів, сеанси й керовані дослідження.
|
||||
- [Каталог інструментів](../mcp-contracts.md#generated-catalog): створюваний під час збірки перелік інструментів, провайдерів і команд CLI.
|
||||
|
||||
@@ -264,6 +264,7 @@ Bắt đầu với [hướng dẫn thực hành](https://rea.tools/vi/guides/) t
|
||||
|
||||
- [Cài đặt và thiết lập](../installation.md): đăng ký tác nhân, cấu hình trình phân tích, cập nhật và gỡ cài đặt.
|
||||
- [Kiểm tra mức sẵn sàng và khắc phục sự cố](../installation.md#check-readiness-for-your-task): chẩn đoán một tác nhân hoặc bộ máy phân tích cụ thể.
|
||||
- [GGUF / safetensors](../model-artifacts.md)
|
||||
- [CLI và Evidence](../cli.md): lệnh, lựa chọn trình phân tích, ảnh chụp trạng thái, nhập/xuất và trạng thái thoát.
|
||||
- [Hợp đồng MCP](../mcp-contracts.md) và [prompt cho tác nhân](../mcp-prompts.md): kết quả công cụ, phiên làm việc và điều tra có hướng dẫn.
|
||||
- [Danh mục công cụ](../mcp-contracts.md#generated-catalog): danh sách công cụ, trình phân tích và lệnh CLI được tạo khi dựng dự án.
|
||||
|
||||
@@ -270,6 +270,7 @@ npx rea-agents@latest setup
|
||||
|
||||
- [安裝與設定](../installation.md):AI 代理註冊、提供者組態、更新和卸載。
|
||||
- [就緒檢查與故障排查](../installation.md#check-readiness-for-your-task):診斷特定 AI 代理或分析引擎。
|
||||
- [GGUF / safetensors](../model-artifacts.md)
|
||||
- [CLI 與 Evidence](../cli.md):命令、提供者選擇、快照、匯入/匯出和退出狀態。
|
||||
- [MCP 契約](../mcp-contracts.md)和[AI 代理提示詞](../mcp-prompts.md):工具結果、會話和引導式調查。
|
||||
- [工具目錄](../mcp-contracts.md#generated-catalog):建置時生成的工具、提供者和 CLI 命令清單。
|
||||
|
||||
@@ -270,6 +270,7 @@ npx rea-agents@latest setup
|
||||
|
||||
- [安装与设置](../installation.md):智能体注册、提供方配置、更新和卸载。
|
||||
- [就绪检查与故障排查](../installation.md#check-readiness-for-your-task):诊断特定智能体或分析引擎。
|
||||
- [GGUF / safetensors](../model-artifacts.md)
|
||||
- [CLI 与 Evidence](../cli.md):命令、提供方选择、快照、导入/导出和退出状态。
|
||||
- [MCP 契约](../mcp-contracts.md)和[智能体提示词](../mcp-prompts.md):工具结果、会话和引导式调查。
|
||||
- [工具目录](../mcp-contracts.md#generated-catalog):构建时生成的工具、提供方和 CLI 命令清单。
|
||||
|
||||
@@ -17,7 +17,7 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement
|
||||
13,src/domain/errors.ts:EvidenceIntegrityError,Evidence validation fails,Raw constructor message,May expose validation internals and lacks recovery,"Evidence is invalid or has changed. Recreate or re-import it, then try again.",implemented,focused assertions passed; full `npm run check` passed
|
||||
14,src/domain/errors.ts:UnknownRegistryError,Evidence-state mutation fails,"Residual unknown registry mutation failed: {reason}",Exposes internal subsystem and state token,"Missing IDs identify the session and retry; concurrent revision conflicts refresh current state.",implemented,focused assertions passed; full `npm run check` passed
|
||||
15,src/domain/errors.ts:ProcessCaptureError,Process capture fails,Raw constructor message,May expose process and cleanup internals,"Process capture could not complete. Check command access and limits, then try again.",implemented,focused assertions passed; full `npm run check` passed
|
||||
16,src/domain/errors.ts:AnalysisTimeoutError,Analysis deadline expires,"REA's {operation} operation reached its {timeoutMs}ms server/provider deadline; the MCP client may have ended the call earlier",Explains the two deadline owners and avoids blind retry,"Inspect session/provider state first. If the MCP client ended the call, raise its tool deadline above {timeoutMs}ms; otherwise retry with a smaller request and do not repeat an unchanged call.",implemented,verified by projection tests and real MCP/Codex CLI timeout-recovery probes
|
||||
16,src/domain/errors.ts:AnalysisTimeoutError,Analysis deadline expires,"REA's {operation} operation reached its {timeoutMs}ms server/provider deadline; extending only the client deadline does not change this budget",Returned server deadline is distinct from a host timeout that discards the response,"Inspect partial Evidence and reported cleanup state; diagnose the provider or reduce supported work without assuming adjustable limits. JavaScript specifically names analysis_timeout_ms and exact source_paths; omit an unneeded filter rather than passing an empty array.",implemented,projection and public CLI/MCP timeout regression coverage; client recovery also taught during initialization and in the shipped skill
|
||||
17,src/domain/errors.ts:HopperTimeoutError,Provider deadline expires,"Hopper did not respond within {timeoutMs}ms",Exposes provider and internal deadline,"Analysis took too long. Try a smaller request, then run `rea doctor` if it continues.",implemented,focused assertions passed; full `npm run check` passed
|
||||
18,src/domain/errors.ts:ProviderSelectionError,Deep-provider selection fails,"Analysis provider selection failed ({reason}) for {requestedProviderId}; candidates: {candidateIds}",Retains non-secret local selection context that needs a safe structured projection,Reason-specific guidance with stable candidate IDs and actionable rejection diagnostics,implemented,focused registry/CLI/MCP assertions passed; full `npm run check` passed
|
||||
20,src/domain/errors.ts:AnalysisInputError,Parsed analysis input invalid,"Invalid analysis input for {operation}",Exposes operation identifier and lacks correction,"Analysis input is invalid. Check the arguments and try again.",implemented,focused assertions passed; full `npm run check` passed
|
||||
|
||||
|
@@ -0,0 +1,74 @@
|
||||
"""Count verified Go tasks; tokenizer estimates are not model usage or billing."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
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())
|
||||
log = path.with_suffix(".log")
|
||||
scope = next(json.loads(line) for line in reversed(log.read_text().splitlines())
|
||||
if line.startswith('{"resource_scope":'))
|
||||
observed = scope.get("payload_resource_observation") or {}
|
||||
events = observed.get("resource_events", {})
|
||||
memory = events.get("memory.events", {})
|
||||
verified = (report["status"] == "observed"
|
||||
and not report["cleanup_failures"] and not report["receipt_failures"]
|
||||
and scope["abort"] is None
|
||||
and scope.get("payload_resource_observation_verified") is True
|
||||
and observed.get("payload_cleanup_verified") is True
|
||||
and observed.get("payload_exit_code") == 0
|
||||
and all(memory.get(key) == 0 for key in ["high", "max", "oom", "oom_kill"])
|
||||
and events.get("pids.events", {}).get("max") == 0)
|
||||
count = lambda value: len(encoding.encode(value, disallowed_special=()))
|
||||
inputs = count(compact([{key: tool[key] for key in ["name", "description", "inputSchema"]}
|
||||
for tool in catalog["tools"]]))
|
||||
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")
|
||||
cpu = observed.get("cpu_stat", {}).get("usage_usec")
|
||||
return {"case": path.name, "completed_with_verified_cleanup": verified,
|
||||
"mode": report["mode"], "warm_diagnostic": report["warm_diagnostic"],
|
||||
"answer": report["answer"], "parent_evidence_id": report["parent_evidence_id"],
|
||||
"artifact_sha256": report["artifact_sha256"],
|
||||
"complete_result_sha256": report["complete_result_file_sha256"],
|
||||
"calls": report["calls"], "wall_seconds": scope["elapsed_seconds"],
|
||||
"cpu_seconds": None if cpu is None else cpu / 1e6,
|
||||
"kernel_family_peak_bytes": observed.get("memory_peak_bytes"),
|
||||
"events_at_payload_exit": events,
|
||||
"final_whole_scope_events": scope.get("resource_events_final"),
|
||||
"token_proxy": {"input_catalog": inputs, "arguments": arguments,
|
||||
"text_replies": replies, "combined": inputs + arguments + replies,
|
||||
"complete_transport_catalog": count(compact(catalog))},
|
||||
"sha256": {file.name: hashlib.sha256(file.read_bytes()).hexdigest()
|
||||
for file in [path / "report.json", path / "catalog.json",
|
||||
path / "transcript.json", log]}}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
parser.add_argument("cases", type=Path, nargs="+")
|
||||
args = parser.parse_args()
|
||||
encoding = tiktoken.get_encoding("cl100k_base")
|
||||
records = [inspect(path, encoding) for path in args.cases]
|
||||
receipt = {"tiktoken": tiktoken.__version__, "encoding": encoding.name,
|
||||
"actual_model_usage": None,
|
||||
"projection": "All discovered input schemas once, plus every call's arguments and text replies, including export. Full transport catalog counted separately; no model turn or host injection measured.",
|
||||
"recordings": records}
|
||||
args.output.write_text(json.dumps(receipt, ensure_ascii=False, indent=2) + "\n")
|
||||
assert all(record["completed_with_verified_cleanup"] for record in records)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,81 @@
|
||||
"""Pin a completed Go report only after checking native build text and file bytes."""
|
||||
import argparse
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--evidence", type=Path, required=True)
|
||||
parser.add_argument("--input", type=Path, required=True)
|
||||
parser.add_argument("--go", type=Path, required=True)
|
||||
parser.add_argument("--dependency", required=True)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
source = args.input.read_bytes()
|
||||
evidence = json.loads(args.evidence.read_text())
|
||||
if "records" in evidence:
|
||||
evidence = next(record for record in evidence["records"]
|
||||
if record["operation"] == "inspect_go_binary"
|
||||
and record["parameters"] == {"path": str(args.input)})
|
||||
report = evidence["normalized_result"]
|
||||
info = report["build_info"]
|
||||
digest = hashlib.sha256(source).hexdigest()
|
||||
assert report["artifact"] == {"path": str(args.input), "sha256": digest,
|
||||
"bytes": len(source)}
|
||||
native = subprocess.check_output(
|
||||
[str(args.go), "version", "-m", str(args.input)], text=True)
|
||||
lines = native.splitlines()
|
||||
assert info["go_version"] == lines[0].rsplit(" ", 1)[1]
|
||||
text = "\n".join(line.removeprefix("\t") for line in lines[1:]) + "\n"
|
||||
assert info["module_text"] == text
|
||||
for name in ["version", "module"]:
|
||||
location = info[name + "_location"]
|
||||
key = ("go_version" if name == "version" else "module") + "_bytes_base64"
|
||||
assert source[location["offset"]:location["offset"] + location["bytes"]] == base64.b64decode(info[key])
|
||||
# Module identity columns are literal tab-separated fields. Preserve empty
|
||||
# sums, development versions, original ordering and local replacements.
|
||||
dependencies = []
|
||||
main_module = None
|
||||
previous = None
|
||||
for line in text.splitlines():
|
||||
columns = line.split("\t")
|
||||
tag = columns[0]
|
||||
if tag not in ["dep", "mod", "=>"]:
|
||||
continue
|
||||
assert len(columns) in [3, 4], columns
|
||||
item = {"path": columns[1], "version": columns[2],
|
||||
"sum": columns[3] if len(columns) == 4 else None}
|
||||
if tag == "=>":
|
||||
assert previous is not None
|
||||
previous["replacement"] = item
|
||||
else:
|
||||
item["replacement"] = None
|
||||
previous = item
|
||||
if tag == "mod":
|
||||
main_module = item
|
||||
else:
|
||||
dependencies.append(item)
|
||||
assert info["module"]["main"] == main_module
|
||||
assert info["module"]["dependencies"] == dependencies
|
||||
answer = {"go_version": info["go_version"], "main": main_module,
|
||||
"runtime_dependencies": "unknown",
|
||||
"dependencies": [{"index": index, **item}
|
||||
for index, item in enumerate(dependencies)
|
||||
if item["path"] == args.dependency]}
|
||||
complete = args.output.with_suffix(".complete-result.json")
|
||||
complete.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n")
|
||||
receipt = {"artifact_sha256": digest, "dependency_path": args.dependency,
|
||||
"dependency_count": len(dependencies), "answer": answer,
|
||||
"complete_result_path": str(complete.resolve()),
|
||||
"native_text_sha256": hashlib.sha256(native.encode()).hexdigest(),
|
||||
"native_go_path": str(args.go),
|
||||
"native_go_sha256": hashlib.sha256(args.go.read_bytes()).hexdigest()}
|
||||
args.output.write_text(json.dumps(receipt, ensure_ascii=False, indent=2) + "\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,195 @@
|
||||
# Selected Go build metadata
|
||||
|
||||
Addresses [#5499](https://github.com/morluto/rea/issues/5499). A compiler/main-module
|
||||
identity question previously required the complete Go report, including raw framed
|
||||
bytes, base64, module text and every parsed dependency. Opt-in `detail: summary`
|
||||
retains the same complete parent and returns identity, source ranges, counts and
|
||||
explicit unknowns. Default complete delivery remains compatible.
|
||||
|
||||
`inspect_analysis_view` reuses that parent for `kind: go`, collection
|
||||
`dependencies` or `settings`. Exact paths/keys and original indices intersect;
|
||||
fields select columns, while every row keeps its original index. Pagination
|
||||
counts matching rows in producer order, including duplicate module paths. The
|
||||
view never reopens a target or reparses module text. Unknown lines and original
|
||||
bytes remain in the complete parent. CLI summaries save that parent in a new
|
||||
Evidence bundle before returning their projection.
|
||||
|
||||
Only immutable authenticated Evidence results reuse schema validation, keyed
|
||||
by their object identity for the Evidence lifetime. Mutable inline results are
|
||||
validated on each call. Subject identity is checked separately on every view.
|
||||
There is no live-file cache. Only page rows are projected; inherited messages
|
||||
use the existing bounded, deduplicating view-message owner.
|
||||
|
||||
## Question and independent checks
|
||||
|
||||
The main artifact is the official GitHub CLI v2.102.0 Linux amd64 release:
|
||||
42,086,560 bytes, SHA-256
|
||||
`7469124f706944133d6a169691dd1c6c3511b12e85878d255e044e2948df4c9b`.
|
||||
The archive digest matches its official GitHub asset metadata. Its embedded
|
||||
compiler is `go1.27.1`, main module `github.com/cli/cli/v2@v2.102.0`, and it has
|
||||
165 parsed dependency records. The selected `golang.org/x/crypto@v0.57.0` record
|
||||
keeps original index 152. These are embedded observations, not authentication
|
||||
of the compiler, supply chain or runtime behavior.
|
||||
|
||||
Native `go version -m` independently matches module text, main identity and all
|
||||
165 dependency identities in original order. Original file ranges match the
|
||||
reported compiler and framed module bytes. Before/after exports have identical
|
||||
complete normalized results and parent Evidence identity. The small real case
|
||||
is the installed 12,366,352-byte Go tool: embedded `go1.23.5`, no decoded main
|
||||
module and zero retained dependencies, with dependency coverage still unknown.
|
||||
REA reads both inputs without executing them. The installed Go tool also runs
|
||||
as the trusted native verifier; the downloaded GitHub CLI binary is never
|
||||
executed.
|
||||
|
||||
Timing baseline is main `ee12d517a25d34853d40e31fc039c7d1576c60e1`. Packaged runtimes
|
||||
and source patch are pinned in the [receipt](go-build-views.json); later changes
|
||||
do not relabel these timings. Node 24.18.0/npm 11.16.0, MCP SDK 2.3.1, Linux
|
||||
x86-64. Fresh processes use unflushed OS caches; the SDK controller pre-reads
|
||||
the input for verification. These are not disk-cold measurements.
|
||||
|
||||
## Actual client context, including every request
|
||||
|
||||
The [wire driver](go-build-wire.py) starts actual Codex app-server 0.163.0 with a
|
||||
fresh account/workspace and a loopback deterministic Responses SSE backend.
|
||||
It starts a scripted turn and real MCP calls, with **no AI inference or paid
|
||||
service**. All 210 tools remain discovered/advertised. Full input/output
|
||||
profiles are explicit, and each relevant tool has a 30,000-token output budget
|
||||
so complete JSON is delivered without retries or truncation. This does not
|
||||
qualify another client's defaults or a real model's reasoning/schema acceptance.
|
||||
|
||||
All original facts and parent identities match the native oracle. Export cases
|
||||
also verify the complete parent. The [counter](go-build-wire-costs.py) counts
|
||||
**every captured request**, including repeated schemas and accumulated tool
|
||||
outputs, using `tiktoken 0.12.0` / `cl100k_base` on compact request JSON. These are
|
||||
serialization-token proxies; actual model rendering, usage and cache billing
|
||||
remain unmeasured.
|
||||
|
||||
| Completed question | Requests before → after | Whole request JSON proxy before → after |
|
||||
| ------------------------------------------------------ | ----------------------: | --------------------------------------: |
|
||||
| Compiler, main module and dependency count | 2 → 2 | 158,529 → 123,368 (22.2% less) |
|
||||
| Compiler, main module and one selected dependency | 2 → 3 | 158,538 → 186,812 (17.8% more) |
|
||||
| Same selected dependency question plus Evidence export | 3 → 4 | 256,342 → 250,352 (2.3% less) |
|
||||
|
||||
The actual REA namespace costs 51,821 → 51,983 proxy tokens **per request**.
|
||||
An extra round can outweigh a smaller reply. Summary is useful when identity
|
||||
and counts already answer the question; complete delivery remains useful for
|
||||
an initial compound question. Retained views provide selected follow-up facts,
|
||||
without a claim that every such composition saves total context.
|
||||
|
||||
One recording per cell: identity wall/CPU/family peak is
|
||||
6.970 s / 6.331 s / 861.5 MiB before and 7.069 s / 6.457 s / 857.0 MiB after.
|
||||
Compound answer-only is 6.973 / 6.333 / 865.0 before and 6.969 / 6.348 / 861.6
|
||||
after. Export is 7.175 / 6.537 / 866.7 before and 7.274 / 6.568 / 872.6 after.
|
||||
These scripted-client observations establish no universal latency or memory
|
||||
gain, and do not measure model inference time.
|
||||
|
||||
## Direct SDK task and cold/warm costs
|
||||
|
||||
The [SDK driver](go-build-views.mjs) discovers all tools, answers the compound
|
||||
question, verifies exact facts and exports complete Evidence. Before: one
|
||||
complete inspection and export. After: summary, selected dependency and export.
|
||||
For the zero-dependency small case, the summary avoids an unnecessary empty
|
||||
query. Two repetitions alternate version/target order. All 208 other named
|
||||
catalog definitions agree; only Go inspection and the retained-view tool change.
|
||||
|
||||
| Input | Whole wall s before → after | Family CPU s before → after | Kernel family peak MiB before → after |
|
||||
| ---------- | --------------------------- | --------------------------- | ------------------------------------- |
|
||||
| GitHub CLI | 4.855–4.952 → 6.370–6.564 | 4.682–4.725 → 6.175–6.400 | 671.2–680.1 → 846.4–868.7 |
|
||||
| Go tool | 4.349–4.351 → 4.250–4.450 | 4.093–4.140 → 4.092–4.179 | 548.4–548.6 → 544.3–555.6 |
|
||||
|
||||
Cold selected-query calls take 1.719–1.751 s in the SDK task, including its
|
||||
output-schema validation. A separate warm diagnostic takes 1.712 s for the first
|
||||
query and 32.86 ms for the repeat in one session; both the client validator and
|
||||
retained facts are warm. This is not a paired whole-task speedup. The complete
|
||||
warm diagnostic is separately recorded, not mixed into the eight matched runs.
|
||||
|
||||
The [SDK counter](go-build-view-costs.py) counts the discovered input catalog
|
||||
once plus all arguments and text replies, including export: 229,183 → 195,643
|
||||
for GitHub CLI and 194,854 → 194,724 for the small case. Catalog cost grows
|
||||
193,192 → 193,652. This catalog-once projection is **not** the actual host
|
||||
exchange above; omitting repeated schemas would hide the compound-task penalty.
|
||||
|
||||
All qualified recordings use one CPU, a 4 GiB hard/3 GiB early family memory
|
||||
boundary, no swap and 192 tasks. Verified payload exits are zero, only the
|
||||
observer remains, and observed memory/task resource events are zero. Kernel
|
||||
family peaks include attributed file cache. Final whole-scope counters are
|
||||
unavailable. Release acquisition, oracle preparation and compilation are outside
|
||||
the timed tasks. An initial symlink input was correctly refused; qualification
|
||||
explicitly selects its regular target. One development static check was stopped
|
||||
by the host-available-memory guard, with owned cleanup verified and no observed
|
||||
OOM; the unchanged guard succeeded after headroom recovered.
|
||||
|
||||
## Coverage and reproduction
|
||||
|
||||
Executed: 10 files / 202 tests covering existing Go readers, mappings, provider,
|
||||
CLI/MCP and Rust readers, plus selected rows, duplicate paths, empty filters,
|
||||
identity-only fields, malformed/mutable parents, source conflicts, bounded
|
||||
messages and unavailable metadata. Actual CLI complete output is identical
|
||||
before/after; summary bundles retain that exact parent, and file-based CLI
|
||||
selected queries match the native oracle. Full `npm run check` passed.
|
||||
After rebasing onto main `c374c2fe7`, the 202 focused tests, full
|
||||
`npm run check` and `npm run docs:check` (including five README consistency
|
||||
tests) pass again. A real SDK selected query and complete export also pass
|
||||
against that rebuilt runtime, retaining the original parent identity and all
|
||||
165 dependencies. These checks do not relabel the frozen timing cohort.
|
||||
|
||||
A review found that a transcript-write failure could skip the SDK driver's
|
||||
cleanup. The shared finalizer now closes owned resources before publishing the
|
||||
optional transcript and records publication failures separately. All 11 receipt
|
||||
failure tests pass. A real selected SDK task and full export pass again; the
|
||||
same task with an injected transcript-write failure exits 1, preserves its
|
||||
answer and failure receipt, and verifies that the process family was cleaned.
|
||||
These qualification runs do not replace the frozen performance measurements.
|
||||
|
||||
A second review found that a valid external parent could disagree with its
|
||||
report's architecture. Go domain now owns the existing architecture-to-subject
|
||||
mapping; both Evidence production and every Go view use it. The view checks
|
||||
mapped architecture as well as path, digest and format, including `null` for
|
||||
unrecognized report names. All 213 focused tests pass, including authenticated
|
||||
inline/file conflicts and known-alias/unknown-architecture cases.
|
||||
|
||||
Initial CI also exposed two existing baseline failures. On unmodified `c374c2fe7`,
|
||||
`src/cliJsonPaths.test.ts` has the same three URL-message assertion failures.
|
||||
The duplication gate is already above its 3.5% threshold: 31,737 / 899,602 lines
|
||||
(3.52789%) on baseline versus 31,748 / 900,474 (3.52570%) on the initial PR head.
|
||||
These were reproduced separately with the pinned toolchain, bounded resources
|
||||
and verified cleanup; this PR leaves those unrelated failures unchanged.
|
||||
|
||||
Final qualification rebased onto main `cf5194f68`, preserving its new IL2CPP
|
||||
view contract alongside the Go additions. All 258 tests in 12 files pass,
|
||||
including 32 IL2CPP view tests and the now-fixed CLI path assertions. Full
|
||||
`npm run check`, `npm run docs:check` plus five README tests, and the real SDK
|
||||
selected query/full export pass again. Complete parent identity and all 165
|
||||
dependencies remain exact. Duplication now passes: baseline 31,644 / 904,778
|
||||
lines (3.49743%), PR 31,612 / 905,740 (3.49018%), against 3.5%.
|
||||
One latest-main build was guard-stopped during a host-memory dip; its owned
|
||||
scope was verified inactive with zero OOM events, then the unchanged guard's
|
||||
retry passed. These results qualify the rebased source, not new task timings.
|
||||
|
||||
The broader metadata audit also read three real Rust binaries. Alpine ripgrep
|
||||
14.1.1-r0's original `.dep-v0` matches native `readelf` coordinates and independent
|
||||
zlib/JSON decoding: 42 packages, 88 indexed edges, root index 33. The other two
|
||||
have no dependency section, which remains unknown coverage. Compiler-path
|
||||
inferences in all three match native read-only ranges and original byte offsets;
|
||||
they remain inferences. These fast metadata reads did not justify a Rust cache.
|
||||
The Alpine archive is pinned by observed SHA-256; APK signatures were not checked.
|
||||
Real provider verification here covers Linux ELF; PE/Mach-O use authored fixtures.
|
||||
|
||||
To reproduce, use built baseline/candidate runtimes and the pinned public input.
|
||||
Create a baseline complete CLI Evidence JSON, then pin the independent oracle:
|
||||
|
||||
```sh
|
||||
python3 experiments/analysis-efficiency/go-build-view-oracle.py \
|
||||
--evidence /tmp/baseline-evidence.json --input /absolute/gh \
|
||||
--go /absolute/go --dependency golang.org/x/crypto --output /tmp/go-oracle.json
|
||||
```
|
||||
|
||||
Run `go-build-views.mjs` with absolute `--runtime`, `--input`, `--oracle` and
|
||||
new `--output` directories, and `--mode complete` or `selected`, under
|
||||
`resource-run.py --observe-payload-exit`. `--warm` adds a separate diagnostic.
|
||||
The wire driver additionally requires pinned `--codex` and `--node`, and takes
|
||||
`--question identity --mode complete|summary` or
|
||||
`--question dependency --mode complete|selected`; `--export` adds export.
|
||||
All wire recordings require the same resource guard for owned-scope cleanup.
|
||||
Counter scripts take `--output` and recording directories, with their guard
|
||||
logs at the same path plus `.log`. Both counters require `tiktoken==0.12.0`.
|
||||
@@ -0,0 +1,228 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
|
||||
import { createRequire } from "node:module";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { finished } from "node:stream/promises";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { parseArgs } from "node:util";
|
||||
import { createRetainedQueryCaller } from "./retained-query-calls.mjs";
|
||||
import { finalizeRetainedQueryReceipt } from "./retained-query-finalization.mjs";
|
||||
|
||||
const { values } = parseArgs({
|
||||
options: {
|
||||
runtime: { type: "string" },
|
||||
input: { type: "string" },
|
||||
oracle: { type: "string" },
|
||||
output: { type: "string" },
|
||||
mode: { type: "string" },
|
||||
warm: { type: "boolean", default: false },
|
||||
},
|
||||
});
|
||||
for (const key of ["runtime", "input", "oracle", "output"])
|
||||
assert.equal(values[key], resolve(values[key]));
|
||||
assert.ok(["complete", "selected"].includes(values.mode));
|
||||
const oracle = JSON.parse(await readFile(values.oracle, "utf8"));
|
||||
const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
||||
const bytes = await readFile(values.input);
|
||||
assert.equal(sha256(bytes), oracle.artifact_sha256);
|
||||
const require = createRequire(join(values.runtime, "package.json"));
|
||||
const { Client } = await import(
|
||||
pathToFileURL(require.resolve("@modelcontextprotocol/client"))
|
||||
);
|
||||
const { StdioClientTransport } = await import(
|
||||
pathToFileURL(require.resolve("@modelcontextprotocol/client/stdio"))
|
||||
);
|
||||
await mkdir(values.output, { mode: 0o700 });
|
||||
const account = await mkdtemp(join(tmpdir(), "rea-go-build-views-"));
|
||||
const report = {
|
||||
question:
|
||||
"Report compiler, main module and the selected embedded dependency; preserve complete original Evidence.",
|
||||
mode: values.mode,
|
||||
warm_diagnostic: values.warm,
|
||||
calls: [],
|
||||
artifact_sha256: oracle.artifact_sha256,
|
||||
};
|
||||
const client = new Client({ name: "rea-go-build-views", version: "1" });
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [join(values.runtime, "scripts/rea.mjs"), "mcp"],
|
||||
env: {
|
||||
HOME: account,
|
||||
PATH: process.env.PATH,
|
||||
NODE_OPTIONS: "--max-old-space-size=2048",
|
||||
UV_THREADPOOL_SIZE: "1",
|
||||
REA_LOG_LEVEL: "silent",
|
||||
},
|
||||
stderr: "pipe",
|
||||
});
|
||||
const serverLog = createWriteStream(join(values.output, "server.log"));
|
||||
const serverLogFinished = finished(serverLog);
|
||||
serverLogFinished.catch(() => {});
|
||||
transport.stderr?.pipe(serverLog);
|
||||
const transcript = [];
|
||||
const call = createRetainedQueryCaller(client, (receipt, event) => {
|
||||
report.calls.push(receipt);
|
||||
transcript.push(event);
|
||||
});
|
||||
async function result(name, input, phase = name) {
|
||||
const reply = await call(name, input, phase);
|
||||
assert.ok(!reply.isError, JSON.stringify(reply));
|
||||
assert.ok(reply.structuredContent);
|
||||
assert.equal(reply.content.length, 1);
|
||||
assert.equal(reply.content[0].type, "text");
|
||||
assert.deepEqual(JSON.parse(reply.content[0].text), reply.structuredContent);
|
||||
return reply.structuredContent;
|
||||
}
|
||||
try {
|
||||
await client.connect(transport);
|
||||
const catalog = await client.listTools();
|
||||
assert.equal(catalog.nextCursor, undefined);
|
||||
report.catalog_tools = catalog.tools.length;
|
||||
await writeFile(join(values.output, "catalog.json"), JSON.stringify(catalog));
|
||||
const definitions = new Map(catalog.tools.map((tool) => [tool.name, tool]));
|
||||
const original = client.callTool.bind(client);
|
||||
client.callTool = (input, options) =>
|
||||
original(input, {
|
||||
...options,
|
||||
timeout: 60000,
|
||||
toolDefinition: definitions.get(input.name),
|
||||
});
|
||||
const inspected = await result(
|
||||
"inspect_go_binary",
|
||||
{
|
||||
path: values.input,
|
||||
...(values.mode === "selected" ? { detail: "summary" } : {}),
|
||||
},
|
||||
"inspection:first",
|
||||
);
|
||||
let answer;
|
||||
if (values.mode === "complete") {
|
||||
report.parent_evidence_id = inspected.evidence_id;
|
||||
const info = inspected.normalized_result.build_info;
|
||||
assert.ok(info);
|
||||
answer = {
|
||||
go_version: info.go_version,
|
||||
main: info.module.main,
|
||||
runtime_dependencies: "unknown",
|
||||
dependencies: info.module.dependencies.flatMap((row, index) =>
|
||||
row.path === oracle.dependency_path ? [{ index, ...row }] : [],
|
||||
),
|
||||
};
|
||||
} else {
|
||||
const summary = inspected.normalized_result;
|
||||
assert.equal(summary.kind, "summary");
|
||||
report.parent_evidence_id = summary.parent_evidence_id;
|
||||
assert.equal(summary.source_coverage.runtime_dependencies, "unknown");
|
||||
assert.equal(
|
||||
summary.summary.build_info.module.available_counts.dependencies,
|
||||
oracle.dependency_count,
|
||||
);
|
||||
const input = {
|
||||
source: {
|
||||
kind: "retained-evidence",
|
||||
evidence_id: report.parent_evidence_id,
|
||||
},
|
||||
view: {
|
||||
kind: "go",
|
||||
collection: "dependencies",
|
||||
paths: [oracle.dependency_path],
|
||||
fields: ["path", "version", "sum", "replacement"],
|
||||
offset: 0,
|
||||
limit: 64,
|
||||
},
|
||||
};
|
||||
let dependencies = [];
|
||||
if (summary.summary.build_info.module.available_counts.dependencies > 0) {
|
||||
const selected = await result(
|
||||
"inspect_analysis_view",
|
||||
input,
|
||||
"retained-query:first",
|
||||
);
|
||||
assert.equal(selected.normalized_result.coverage.exhausted, true);
|
||||
assert.equal(
|
||||
selected.normalized_result.source_coverage.runtime_dependencies,
|
||||
"unknown",
|
||||
);
|
||||
dependencies = selected.normalized_result.page.rows;
|
||||
if (values.warm) {
|
||||
const warm = await result(
|
||||
"inspect_analysis_view",
|
||||
input,
|
||||
"retained-query:warm-diagnostic",
|
||||
);
|
||||
assert.deepEqual(warm, selected);
|
||||
}
|
||||
}
|
||||
answer = {
|
||||
go_version: summary.summary.build_info.go_version,
|
||||
main: summary.summary.build_info.module.main,
|
||||
runtime_dependencies: "unknown",
|
||||
dependencies,
|
||||
};
|
||||
}
|
||||
assert.deepEqual(answer, oracle.answer);
|
||||
report.answer = answer;
|
||||
await result("export_evidence_bundle", {
|
||||
path: join(values.output, "evidence.json"),
|
||||
});
|
||||
const bundle = JSON.parse(
|
||||
await readFile(join(values.output, "evidence.json"), "utf8"),
|
||||
);
|
||||
const parent = bundle.records.find(
|
||||
(e) => e.evidence_id === report.parent_evidence_id,
|
||||
);
|
||||
assert.ok(parent);
|
||||
assert.equal(parent.operation, "inspect_go_binary");
|
||||
assert.deepEqual(parent.parameters, { path: values.input });
|
||||
assert.equal(
|
||||
parent.normalized_result.artifact.sha256,
|
||||
oracle.artifact_sha256,
|
||||
);
|
||||
// The pre-change normalized result was independently checked against native
|
||||
// go version -m and its file-backed bytes before pinning this exact JSON.
|
||||
const expected = JSON.parse(
|
||||
await readFile(oracle.complete_result_path, "utf8"),
|
||||
);
|
||||
assert.deepEqual(parent.normalized_result, expected);
|
||||
assert.equal(
|
||||
parent.normalized_result.build_info.module.dependencies.length,
|
||||
oracle.dependency_count,
|
||||
);
|
||||
for (const name of ["version", "module"]) {
|
||||
const info = parent.normalized_result.build_info;
|
||||
const location = info[name + "_location"];
|
||||
const encoded =
|
||||
info[(name === "version" ? "go_version" : "module") + "_bytes_base64"];
|
||||
assert.deepEqual(
|
||||
bytes.subarray(location.offset, location.offset + location.bytes),
|
||||
Buffer.from(encoded, "base64"),
|
||||
);
|
||||
}
|
||||
assert.equal(sha256(await readFile(values.input)), oracle.artifact_sha256);
|
||||
report.oracle_file_sha256 = sha256(await readFile(values.oracle));
|
||||
report.complete_result_file_sha256 = sha256(
|
||||
await readFile(oracle.complete_result_path),
|
||||
);
|
||||
report.status = "observed";
|
||||
} catch (cause) {
|
||||
report.status = "failed";
|
||||
report.failure = String(cause?.stack ?? cause);
|
||||
process.exitCode = 1;
|
||||
} finally {
|
||||
if (
|
||||
!(await finalizeRetainedQueryReceipt({
|
||||
report,
|
||||
client,
|
||||
transport,
|
||||
account,
|
||||
serverLog,
|
||||
serverLogFinished,
|
||||
output: values.output,
|
||||
transcript,
|
||||
}))
|
||||
)
|
||||
process.exitCode = 1;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
"""Count entire captured Codex request sequences, without claiming model usage."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import tiktoken
|
||||
|
||||
|
||||
def compact(value):
|
||||
return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
|
||||
|
||||
|
||||
def request_text(body):
|
||||
pieces = []
|
||||
if isinstance(body.get("instructions"), str):
|
||||
pieces.append(body["instructions"])
|
||||
for item in body.get("input", []):
|
||||
kind = item.get("type")
|
||||
if kind == "function_call_output":
|
||||
pieces.append(item["output"])
|
||||
elif kind == "function_call":
|
||||
pieces.append(item["arguments"])
|
||||
elif kind == "message" or "role" in item:
|
||||
for part in item.get("content", []):
|
||||
if isinstance(part, dict) and isinstance(part.get("text"), str):
|
||||
pieces.append(part["text"])
|
||||
return pieces
|
||||
|
||||
|
||||
def inspect(root, encoding):
|
||||
report = json.loads((root / "report.json").read_text())
|
||||
assert (report["status"] == "passed" and not report["cleanup_failures"]
|
||||
and report["appserver_exit_code"] == 0)
|
||||
requests = json.loads((root / "model-requests.json").read_text())
|
||||
whole, namespaces, texts = [], [], []
|
||||
count = lambda value: len(encoding.encode(value, disallowed_special=()))
|
||||
for event in requests:
|
||||
body = event["body"]
|
||||
whole.append(count(compact(body)))
|
||||
namespace = next(tool for tool in body["tools"]
|
||||
if tool.get("type") == "namespace"
|
||||
and tool["name"] == "mcp__rea_audit")
|
||||
namespaces.append(count(compact(namespace)))
|
||||
texts.append(sum(count(piece) for piece in request_text(body)))
|
||||
log = root.with_suffix(".log")
|
||||
scope = next(json.loads(line) for line in reversed(log.read_text().splitlines())
|
||||
if line.startswith('{"resource_scope":'))
|
||||
observed = scope["payload_resource_observation"]
|
||||
assert (scope["abort"] is None and scope["payload_resource_observation_verified"]
|
||||
and observed["payload_cleanup_verified"] and observed["payload_exit_code"] == 0)
|
||||
memory = observed["resource_events"]["memory.events"]
|
||||
assert all(memory[key] == 0 for key in ["high", "max", "oom", "oom_kill"])
|
||||
assert observed["resource_events"]["pids.events"]["max"] == 0
|
||||
return {"case": root.name, "question": report["question_kind"],
|
||||
"request_count": len(requests), "whole_request_json_proxy": whole,
|
||||
"whole_request_json_proxy_sum": sum(whole),
|
||||
"rea_namespace_json_proxy": namespaces,
|
||||
"rea_namespace_json_proxy_sum": sum(namespaces),
|
||||
"unescaped_instruction_and_input_text_proxy": texts,
|
||||
"rea_namespace_plus_text_proxy_sum": sum(namespaces) + sum(texts),
|
||||
"wall_seconds": scope["elapsed_seconds"],
|
||||
"family_cpu_seconds": observed["cpu_stat"]["usage_usec"] / 1e6,
|
||||
"family_peak_mib": observed["memory_peak_bytes"] / 2**20,
|
||||
"completed_with_verified_cleanup": True,
|
||||
"sha256": {file.name: hashlib.sha256(file.read_bytes()).hexdigest()
|
||||
for file in [root / "report.json", root / "model-requests.json",
|
||||
root / "transcript.json", log]}}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
parser.add_argument("cases", type=Path, nargs="+")
|
||||
args = parser.parse_args()
|
||||
encoding = tiktoken.get_encoding("cl100k_base")
|
||||
receipt = {"actual_model_usage": None,
|
||||
"backend": "deterministic local Responses SSE; no AI inference or paid service; not a model-quality or billing test",
|
||||
"client": "Codex app-server 0.163.0", "tiktoken": tiktoken.__version__,
|
||||
"encoding": encoding.name,
|
||||
"records": [inspect(root, encoding) for root in args.cases]}
|
||||
args.output.write_text(json.dumps(receipt, ensure_ascii=False, indent=2) + "\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,292 @@
|
||||
"""Actual Codex wire capture with a deterministic local SSE backend; no AI inference."""
|
||||
import argparse
|
||||
import http.server
|
||||
import threading
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import time
|
||||
import traceback
|
||||
import subprocess
|
||||
|
||||
|
||||
async def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
parser.add_argument("--runtime", type=Path, required=True)
|
||||
parser.add_argument("--codex", type=Path, required=True)
|
||||
parser.add_argument("--node", type=Path, required=True)
|
||||
parser.add_argument("--input-profile", choices=["full", "compact"], default="full")
|
||||
parser.add_argument("--output-profile", choices=["full", "off"], default="full")
|
||||
parser.add_argument("--input", type=Path, required=True)
|
||||
parser.add_argument("--oracle", type=Path, required=True)
|
||||
parser.add_argument("--expected-tools", type=int, default=210)
|
||||
parser.add_argument("--mode", choices=["complete", "selected", "summary"], required=True)
|
||||
parser.add_argument("--export", action="store_true")
|
||||
parser.add_argument("--question", choices=["identity", "dependency"], default="dependency")
|
||||
args = parser.parse_args()
|
||||
assert args.question != "identity" or args.mode != "selected"
|
||||
assert args.mode != "summary" or args.question == "identity"
|
||||
oracle=json.loads(args.oracle.read_text())
|
||||
target=str(args.input)
|
||||
assert args.input.is_absolute() and args.oracle.is_absolute() and args.output.is_absolute()
|
||||
assert hashlib.sha256(args.input.read_bytes()).hexdigest()==oracle["artifact_sha256"]
|
||||
def delivered(pieces):
|
||||
assert isinstance(pieces,str) and pieces.startswith("Wall time: "), pieces[:300]
|
||||
_,sep,payload=pieces.partition("Output:\n");assert sep
|
||||
value=json.loads(payload)
|
||||
return value["result"] if "result" in value and "normalized_result" not in value else value
|
||||
def outputs_of(body):
|
||||
return {item["call_id"]:delivered(item["output"]) for item in body.get("input",[]) if item.get("type")=="function_call_output"}
|
||||
steps=1+(args.mode=="selected")+args.export
|
||||
args.output.mkdir(mode=0o700)
|
||||
audit_home = args.output / "account"
|
||||
codex_state = audit_home / ".codex"
|
||||
mcp_home = audit_home / "mcp"
|
||||
workspace = audit_home / "workspace"
|
||||
for directory in [codex_state, mcp_home, workspace]:
|
||||
directory.mkdir(parents=True)
|
||||
captured = []
|
||||
class Capture(http.server.BaseHTTPRequestHandler):
|
||||
def log_message(self, *_):
|
||||
pass
|
||||
def do_POST(self):
|
||||
size = int(self.headers["Content-Length"])
|
||||
assert 0 < size <= 16 * 1024 * 1024
|
||||
assert self.headers.get("Content-Encoding", "identity") == "identity"
|
||||
body = json.loads(self.rfile.read(size))
|
||||
captured.append({"path": self.path, "body": body})
|
||||
(args.output / "model-requests.json").write_text(json.dumps(captured) + "\n")
|
||||
index = len(captured)
|
||||
names = [t.get("name", "") for ns in body.get("tools", []) if ns.get("type") == "namespace" and ns.get("name") == "mcp__rea_audit" for t in ns.get("tools", [])]
|
||||
assert index <= steps+1
|
||||
response_id=f"resp_local_{index}"
|
||||
if index <= steps:
|
||||
previous=outputs_of(body)
|
||||
if index==1:
|
||||
operation="inspect_go_binary";arguments={"path":target}
|
||||
if args.mode!="complete":arguments["detail"]="summary"
|
||||
elif args.mode=="selected" and index==2:
|
||||
parent=previous["call_local_1"]["normalized_result"]["parent_evidence_id"]
|
||||
operation="inspect_analysis_view";arguments={"source":{"kind":"retained-evidence","evidence_id":parent},"view":{"kind":"go","collection":"dependencies","paths":[oracle["dependency_path"]],"fields":["path","version","sum","replacement"],"offset":0,"limit":64}}
|
||||
else:
|
||||
operation="export_evidence_bundle";arguments={"path":str(args.output/"evidence.json")}
|
||||
selected=[name for name in names if name.endswith(operation)];assert len(selected)==1
|
||||
item={"type":"function_call","id":f"fc_local_{index}","call_id":f"call_local_{index}","name":selected[0],"namespace":"mcp__rea_audit","arguments":json.dumps(arguments),"status":"completed"}
|
||||
else:
|
||||
item={"type":"message","id":"msg_local_done","role":"assistant","status":"completed","content":[{"type":"output_text","text":"Scripted local Go metadata query complete.","annotations":[]}]}
|
||||
base = {"id": response_id, "object": "response", "model": body.get("model"), "status": "completed", "output": [item]}
|
||||
events = [
|
||||
{"type": "response.created", "response": {**base, "status": "in_progress", "output": []}},
|
||||
{"type": "response.output_item.added", "output_index": 0, "item": {**item, "status": "in_progress"}},
|
||||
{"type": "response.output_item.done", "output_index": 0, "item": item},
|
||||
{"type": "response.completed", "response": base},
|
||||
]
|
||||
wire = "".join("event: " + e["type"] + "\ndata: " + json.dumps(e) + "\n\n" for e in events).encode()
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/event-stream")
|
||||
self.send_header("Content-Length", str(len(wire)))
|
||||
self.end_headers()
|
||||
self.wfile.write(wire)
|
||||
endpoint = http.server.HTTPServer(("127.0.0.1", 0), Capture)
|
||||
endpoint_thread = threading.Thread(target=endpoint.serve_forever, daemon=True)
|
||||
endpoint_thread.start()
|
||||
task_path = f"{args.node.parent}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
mcp_env = {"HOME": str(mcp_home), "PATH": task_path,
|
||||
"NODE_OPTIONS": "--max-old-space-size=2048", "UV_THREADPOOL_SIZE": "1",
|
||||
"REA_LOG_LEVEL": "silent", "REA_MCP_INPUT_SCHEMA_PROFILE": args.input_profile,
|
||||
"REA_MCP_OUTPUT_SCHEMA_PROFILE": args.output_profile}
|
||||
config = 'model_provider = "rea_capture"\nmodel = "rea-wire-probe"\n[analytics]\nenabled = false\n[mcp_servers.rea_audit]\n'
|
||||
config += f'command = {json.dumps(str(args.node))}\n'
|
||||
config += f'args = {json.dumps([str(args.runtime / "scripts/rea.mjs"), "mcp"])}\n'
|
||||
config += 'startup_timeout_sec = 60\ntool_timeout_sec = 60\n'
|
||||
config += '[mcp_servers.rea_audit.env]\n'
|
||||
config += ''.join(f'{key} = {json.dumps(value)}\n' for key, value in mcp_env.items())
|
||||
for name in ['inspect_go_binary','inspect_analysis_view','export_evidence_bundle']:
|
||||
config += f'[mcp_servers.rea_audit.tools.{name}]\napproval_mode = "approve"\noutput_token_limit = 30000\n'
|
||||
config += f'\n[model_providers.rea_capture]\nname = "Local scripted capture"\nbase_url = "http://127.0.0.1:{endpoint.server_port}/v1"\nwire_api = "responses"\nrequires_openai_auth = false\nsupports_websockets = false\nrequest_max_retries = 0\nstream_max_retries = 0\n'
|
||||
(codex_state / "config.toml").write_text(config)
|
||||
(args.output / "declared-config.toml").write_text(config)
|
||||
environment = {"HOME": str(audit_home), "CODEX_HOME": str(codex_state), "PATH": task_path,
|
||||
"NODE_OPTIONS": "--max-old-space-size=2048", "UV_THREADPOOL_SIZE": "1"}
|
||||
report = {"question": "Read embedded Go identity/counts or a selected dependency. Export is a separate task variant.",
|
||||
"client": "Codex app-server 0.163.0", "model_turns_started": 1, "backend": "local deterministic SSE fixture; no AI model or paid service",
|
||||
"actual_model_usage": None, "input_profile": args.input_profile,
|
||||
"output_profile": args.output_profile, "calls": [], "observations": [],
|
||||
"unexpected_server_requests": [], "cleanup_failures": []}
|
||||
client_version = subprocess.check_output([str(args.codex), "--version"], env=environment, cwd=workspace, text=True, timeout=10).strip()
|
||||
assert client_version == "codex-cli 0.163.0", client_version
|
||||
transcript = []
|
||||
pending = {}
|
||||
completed_turn = asyncio.Event()
|
||||
next_id = 0
|
||||
stderr = (args.output / "server.log").open("wb")
|
||||
process = await asyncio.create_subprocess_exec(str(args.codex), "app-server", "--stdio",
|
||||
stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=stderr,
|
||||
cwd=workspace, env=environment, limit=16 * 1024 * 1024)
|
||||
|
||||
async def send(message):
|
||||
encoded = (json.dumps(message, separators=(",", ":")) + "\n").encode()
|
||||
transcript.append({"direction": "client", "bytes": len(encoded), "message": message})
|
||||
process.stdin.write(encoded)
|
||||
await process.stdin.drain()
|
||||
|
||||
async def read_messages():
|
||||
try:
|
||||
while line := await process.stdout.readline():
|
||||
value = json.loads(line)
|
||||
transcript.append({"direction": "server", "bytes": len(line), "message": value})
|
||||
if value.get("method") == "turn/completed":
|
||||
report["turn"] = value["params"]["turn"]
|
||||
completed_turn.set()
|
||||
if "id" in value and "method" not in value:
|
||||
future = pending.pop(value["id"], None)
|
||||
if future and not future.done():
|
||||
future.set_result(value)
|
||||
elif "id" in value:
|
||||
report["unexpected_server_requests"].append(value["method"])
|
||||
await send({"id": value["id"], "error": {"code": -32601, "message": "Unexpected request in scripted metadata probe"}})
|
||||
raise RuntimeError("app-server stdout closed")
|
||||
except Exception as error:
|
||||
for future in pending.values():
|
||||
if not future.done():
|
||||
future.set_exception(error)
|
||||
|
||||
reader = asyncio.create_task(read_messages())
|
||||
|
||||
async def request(method, params):
|
||||
nonlocal next_id
|
||||
assert method in ["initialize", "thread/start", "mcpServerStatus/list", "mcpServer/tool/call", "turn/start"]
|
||||
next_id += 1
|
||||
message = {"id": next_id, "method": method, "params": params}
|
||||
future = asyncio.get_running_loop().create_future()
|
||||
pending[next_id] = future
|
||||
start = time.monotonic()
|
||||
await send(message)
|
||||
reply = await asyncio.wait_for(future, 90)
|
||||
report["calls"].append({"method": method, "elapsed_ms": (time.monotonic() - start) * 1000,
|
||||
"error": reply.get("error")})
|
||||
if "error" in reply:
|
||||
raise RuntimeError(json.dumps(reply["error"])[:4000])
|
||||
return reply["result"]
|
||||
|
||||
try:
|
||||
await request("initialize", {"clientInfo": {"name": "rea_host_probe", "title": "REA host compatibility probe", "version": "1"},
|
||||
"capabilities": {"experimentalApi": True, "explicitGatewayOauth": True}})
|
||||
await send({"method": "initialized"})
|
||||
created = await request("thread/start", {"cwd": str(workspace), "ephemeral": True,
|
||||
"approvalPolicy": "never", "sandbox": "read-only"})
|
||||
thread_id = created["thread"]["id"]
|
||||
assert not created["thread"].get("turns", [])
|
||||
inventory = await request("mcpServerStatus/list", {"threadId": thread_id,
|
||||
"serverName": "rea_audit", "detail": "toolsAndAuthOnly", "limit": 10})
|
||||
assert inventory.get("nextCursor") is None
|
||||
assert len(inventory["data"]) == 1
|
||||
server = inventory["data"][0]
|
||||
assert server["name"] == "rea_audit" and server.get("toolsError") is None
|
||||
tools = list(server["tools"].values())
|
||||
names = [tool["name"] for tool in tools]
|
||||
assert len(names) == len(set(names)) == args.expected_tools
|
||||
assert "inspect_go_binary" in names
|
||||
output_schemas = sum(tool.get("outputSchema") is not None for tool in tools)
|
||||
assert output_schemas == (args.expected_tools if args.output_profile == "full" else 0)
|
||||
raw = json.dumps(inventory, separators=(",", ":"), ensure_ascii=False).encode()
|
||||
(args.output / "catalog.json").write_bytes(raw)
|
||||
report["catalog"] = {"tools": len(tools), "output_schemas": output_schemas,
|
||||
"bytes": len(raw), "sha256": hashlib.sha256(raw).hexdigest(),
|
||||
"server_info": server.get("serverInfo"), "runtime_status": server.get("runtimeStatus")}
|
||||
await request("turn/start", {"threadId": thread_id, "input": [{"type": "text", "text": ("Read the embedded Go compiler, main module and embedded dependency count; state runtime dependency coverage." if args.question=="identity" else "Read the embedded Go compiler, main module and golang.org/x/crypto dependency and state runtime dependency coverage.")+" The local fixture scripts the required tools; no AI inference is used."}]})
|
||||
await asyncio.wait_for(completed_turn.wait(), 90)
|
||||
assert report["turn"]["status"] == "completed", report["turn"]
|
||||
assert len(captured)==steps+1
|
||||
outputs=outputs_of(captured[-1]["body"])
|
||||
assert len(outputs)==steps
|
||||
initial=outputs["call_local_1"]
|
||||
assert initial["normalized_result"]["artifact"]["sha256"]==oracle["artifact_sha256"]
|
||||
if args.question=="identity":
|
||||
info=initial["normalized_result"]["build_info"] if args.mode=="complete" else initial["normalized_result"]["summary"]["build_info"]
|
||||
count=len(info["module"]["dependencies"]) if args.mode=="complete" else info["module"]["available_counts"]["dependencies"]
|
||||
facts={"go_version":info["go_version"],"main":info["module"]["main"],"runtime_dependencies":"unknown","dependency_count":count}
|
||||
parent=initial["evidence_id"] if args.mode=="complete" else initial["normalized_result"]["parent_evidence_id"]
|
||||
elif args.mode=="complete":
|
||||
info=initial["normalized_result"]["build_info"]
|
||||
facts={"go_version":info["go_version"],"main":info["module"]["main"],"runtime_dependencies":"unknown","dependencies":[{"index":i,**d} for i,d in enumerate(info["module"]["dependencies"]) if d["path"]==oracle["dependency_path"]]}
|
||||
parent=initial["evidence_id"]
|
||||
else:
|
||||
info=initial["normalized_result"]["summary"]["build_info"]
|
||||
selected=outputs["call_local_2"]["normalized_result"]
|
||||
assert selected["coverage"]["exhausted"] and selected["source_coverage"]["runtime_dependencies"]=="unknown"
|
||||
facts={"go_version":info["go_version"],"main":info["module"]["main"],"runtime_dependencies":"unknown","dependencies":selected["page"]["rows"]}
|
||||
parent=initial["normalized_result"]["parent_evidence_id"]
|
||||
expected=({key:oracle["answer"][key] for key in ["go_version","main","runtime_dependencies"]}|{"dependency_count":oracle["dependency_count"]}) if args.question=="identity" else oracle["answer"]
|
||||
assert facts==expected
|
||||
report["question_kind"]=args.question
|
||||
if args.export:
|
||||
bundle=json.loads((args.output/"evidence.json").read_text());evidence=next(e for e in bundle["records"] if e["evidence_id"]==parent)
|
||||
assert evidence["normalized_result"]==json.loads(Path(oracle["complete_result_path"]).read_text())
|
||||
report["observations"]={"answer":facts,"parent_evidence_id":parent,"exported":args.export}
|
||||
report["captured_requests"]=len(captured)
|
||||
report["mode"]=args.mode
|
||||
report["request_tool_counts"] = [len(c["body"].get("tools", [])) for c in captured]
|
||||
assert not report["unexpected_server_requests"]
|
||||
assert hashlib.sha256(args.input.read_bytes()).hexdigest()==oracle["artifact_sha256"]
|
||||
report["status"] = "passed"
|
||||
except Exception:
|
||||
report["status"] = "failed"
|
||||
report["failure"] = traceback.format_exc()[-6000:]
|
||||
finally:
|
||||
process.stdin.close()
|
||||
try:
|
||||
await asyncio.wait_for(process.wait(), 15)
|
||||
except asyncio.TimeoutError:
|
||||
report["cleanup_failures"].append("app-server did not exit after stdin close; terminated owned process")
|
||||
process.terminate()
|
||||
try:
|
||||
await asyncio.wait_for(process.wait(), 5)
|
||||
except asyncio.TimeoutError:
|
||||
process.kill()
|
||||
await process.wait()
|
||||
await reader
|
||||
# app-server can acknowledge exit before its MCP child finishes EOF
|
||||
# cleanup. Wait on the allocated resource scope; never target a PID.
|
||||
try:
|
||||
groups = [line[3:] for line in Path("/proc/self/cgroup").read_text().splitlines()
|
||||
if line.startswith("0::")]
|
||||
assert len(groups) == 1
|
||||
group = Path("/sys/fs/cgroup") / groups[0].lstrip("/")
|
||||
assert re.fullmatch(r"rea-efficiency-[0-9a-f]{32}\.scope", group.name)
|
||||
observer = os.getppid()
|
||||
observer_args = Path(f"/proc/{observer}/cmdline").read_bytes().split(b"\0")
|
||||
assert any(Path(os.fsdecode(value)).name == "resource_payload.py" for value in observer_args if value)
|
||||
expected = {os.getpid(), observer}
|
||||
until = time.monotonic() + 10
|
||||
while True:
|
||||
actors = {int(value) for value in (group / "cgroup.procs").read_text().splitlines()}
|
||||
assert expected <= actors
|
||||
remaining = sorted(actors - expected)
|
||||
if not remaining or time.monotonic() >= until:
|
||||
break
|
||||
await asyncio.sleep(0.05)
|
||||
report["owned_scope_drain"] = {"remaining_processes": remaining}
|
||||
if remaining:
|
||||
report["cleanup_failures"].append("owned MCP child did not settle after app-server exit")
|
||||
except (OSError, ValueError, AssertionError) as error:
|
||||
report["owned_scope_drain"] = {"status": "unavailable", "reason": type(error).__name__}
|
||||
report["cleanup_failures"].append("allocated resource-scope cleanup could not be verified")
|
||||
endpoint.shutdown()
|
||||
endpoint.server_close()
|
||||
endpoint_thread.join(5)
|
||||
stderr.close()
|
||||
report["appserver_exit_code"] = process.returncode
|
||||
if process.returncode != 0:
|
||||
report["cleanup_failures"].append(f"app-server exit code: {process.returncode}")
|
||||
(args.output / "transcript.json").write_text(json.dumps(transcript) + "\n")
|
||||
(args.output / "report.json").write_text(json.dumps(report, indent=2) + "\n")
|
||||
print(json.dumps({key: report.get(key) for key in ["status", "failure", "catalog", "observations", "cleanup_failures", "appserver_exit_code"]}))
|
||||
return 0 if report["status"] == "passed" and not report["cleanup_failures"] else 1
|
||||
|
||||
|
||||
raise SystemExit(asyncio.run(main()))
|
||||
@@ -40,6 +40,7 @@ export const finalizeRetainedQueryReceipt = async ({
|
||||
serverLog,
|
||||
serverLogFinished,
|
||||
output,
|
||||
transcript,
|
||||
stdout = process.stdout,
|
||||
}) => {
|
||||
report.cleanup_failures = [];
|
||||
@@ -58,6 +59,10 @@ export const finalizeRetainedQueryReceipt = async ({
|
||||
if (report.cleanup_failures.length > 0) report.retained_workspace = account;
|
||||
|
||||
report.receipt_failures = [];
|
||||
if (transcript !== undefined)
|
||||
await attempt(report.receipt_failures, "transcript-file", () =>
|
||||
writeFile(join(output, "transcript.json"), JSON.stringify(transcript)),
|
||||
);
|
||||
let publication = 0;
|
||||
const saveReport = async () => {
|
||||
// Keep failed publications in the owned output directory for inspection.
|
||||
|
||||
@@ -109,6 +109,23 @@ assert.match(report.cleanup_failures[0],/^workspace:/);
|
||||
assert.equal(report.retained_workspace,malformedAccount);
|
||||
assert.deepEqual((await saved()).failure,originalFailure);
|
||||
assert.deepEqual(JSON.parse(chunks.join('')),report);
|
||||
''')
|
||||
|
||||
def test_transcript_failure_cannot_skip_cleanup_or_lose_original_failure(self):
|
||||
self.run_finalization('''
|
||||
await mkdir(join(output,'transcript.json'));
|
||||
const closed=[];
|
||||
assert.equal(await finalizeRetainedQueryReceipt({...input,transcript:[{phase:'page'}],
|
||||
client:{close:async()=>{closed.push('client');}},
|
||||
transport:{close:async()=>{closed.push('transport');}}}),false);
|
||||
assert.deepEqual(closed,['client','transport']);
|
||||
assert.equal(serverLog.writableEnded,true);
|
||||
await assert.rejects(stat(account),error=>error.code==='ENOENT');
|
||||
assert.deepEqual(report.cleanup_failures,[]);
|
||||
assert.match(report.receipt_failures[0],/^transcript-file:/);
|
||||
assert.strictEqual(report.failure,originalFailure);
|
||||
assert.equal(report.calls[0].elapsed_ms,17);
|
||||
assert.deepEqual(await saved(),report);assert.deepEqual(JSON.parse(chunks.join('')),report);
|
||||
''')
|
||||
|
||||
def test_report_file_refusal_preserves_the_stdout_receipt(self):
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { createInterface } from "node:readline";
|
||||
import { PassThrough, Writable } from "node:stream";
|
||||
|
||||
import {
|
||||
OwnedCommandFailure,
|
||||
prepareOwnedCommand,
|
||||
runOwnedCommand,
|
||||
} from "../../dist/process/OwnedCommand.js";
|
||||
import { ArtifactResourceScope } from "../../dist/artifacts/ArtifactResourceScope.js";
|
||||
import { AnalysisCleanupLease } from "../../dist/artifacts/AnalysisCleanupLease.js";
|
||||
import { retainOwnedCommandCleanup } from "../../dist/providers/OwnedCommandCleanup.js";
|
||||
|
||||
/** Keep command ownership through failure and join cleanup before removing a run. */
|
||||
export function createAgentEvaluationCommands({
|
||||
codex,
|
||||
evaluationRoot,
|
||||
evaluationEnvironment,
|
||||
optionalModel,
|
||||
timeoutMs,
|
||||
repositoryRoot,
|
||||
}) {
|
||||
const resources = new ArtifactResourceScope();
|
||||
const resourceOwner = {
|
||||
retain(owner, resource) {
|
||||
resources.retain({
|
||||
kind: "lease",
|
||||
resource,
|
||||
lease: AnalysisCleanupLease.forCloseOwner(owner, resource),
|
||||
});
|
||||
},
|
||||
run: (work) => resources.run(work),
|
||||
};
|
||||
const diagnosticBytes = 32_768;
|
||||
|
||||
async function execute(
|
||||
command,
|
||||
arguments_,
|
||||
cwd,
|
||||
env,
|
||||
limits,
|
||||
timeoutMessage,
|
||||
) {
|
||||
return resourceOwner.run(async () => {
|
||||
try {
|
||||
// Fresh Darwin helper compilation has its own bounded deadline.
|
||||
await prepareOwnedCommand();
|
||||
return await runOwnedCommand(
|
||||
{
|
||||
command,
|
||||
arguments: arguments_,
|
||||
// CLI shims may execute through a shebang interpreter. Ownership
|
||||
// still uses the launch generation and token, not an argv prefix.
|
||||
expectedCommand: null,
|
||||
cwd,
|
||||
hostEnvironment: env,
|
||||
runId: randomUUID(),
|
||||
sweepTokenOwnedProcesses: true,
|
||||
},
|
||||
{ diagnosticBytes, acceptNonZeroExit: true, ...limits },
|
||||
);
|
||||
} catch (cause) {
|
||||
retainOwnedCommandCleanup(
|
||||
cause,
|
||||
resourceOwner,
|
||||
"agent-evaluation",
|
||||
command,
|
||||
);
|
||||
if (cause instanceof OwnedCommandFailure && cause.reason === "timeout")
|
||||
throw new Error(timeoutMessage, { cause });
|
||||
throw cause;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
async runCodex(prompt, approvedTools) {
|
||||
const arguments_ = [
|
||||
"--no-daemon",
|
||||
"exec",
|
||||
"--ephemeral",
|
||||
"--skip-git-repo-check",
|
||||
"--sandbox",
|
||||
"read-only",
|
||||
"--json",
|
||||
"--color",
|
||||
"never",
|
||||
"-C",
|
||||
evaluationRoot,
|
||||
"-c",
|
||||
'approval_policy="never"',
|
||||
...approvedTools.flatMap((name) => [
|
||||
"-c",
|
||||
`mcp_servers.rea.tools.${name}.approval_mode="approve"`,
|
||||
]),
|
||||
...(optionalModel === undefined ? [] : ["--model", optionalModel]),
|
||||
prompt,
|
||||
];
|
||||
const events = [];
|
||||
const nonJson = [];
|
||||
let nonJsonBytes = 0;
|
||||
const stdout = new PassThrough();
|
||||
const lines = createInterface({ input: stdout });
|
||||
const linesClosed = new Promise((resolve) =>
|
||||
lines.once("close", resolve),
|
||||
);
|
||||
let lineError;
|
||||
lines.on("error", (cause) => {
|
||||
// The owned pipeline reports stream failure or cancellation. Join this
|
||||
// consumer too, without turning its forwarded error into a process crash.
|
||||
lineError = cause;
|
||||
lines.close();
|
||||
});
|
||||
lines.on("line", (line) => {
|
||||
try {
|
||||
events.push(JSON.parse(line));
|
||||
} catch {
|
||||
if (nonJsonBytes >= diagnosticBytes) return;
|
||||
const bytes = Buffer.from(
|
||||
`non-json stdout: ${line.slice(0, diagnosticBytes - nonJsonBytes)}`,
|
||||
);
|
||||
const retained = bytes.subarray(0, diagnosticBytes - nonJsonBytes);
|
||||
if (retained.length > 0) nonJson.push(retained);
|
||||
nonJsonBytes += retained.length;
|
||||
}
|
||||
});
|
||||
try {
|
||||
const result = await execute(
|
||||
codex,
|
||||
arguments_,
|
||||
evaluationRoot,
|
||||
evaluationEnvironment,
|
||||
{ timeoutMs, stdout },
|
||||
`Codex evaluation timed out after ${String(timeoutMs)}ms`,
|
||||
);
|
||||
await linesClosed;
|
||||
if (lineError !== undefined) throw lineError;
|
||||
return {
|
||||
events,
|
||||
exitCode: result.exitCode ?? 1,
|
||||
stderr: Buffer.concat([...nonJson, Buffer.from(result.stderr.text)])
|
||||
.subarray(0, diagnosticBytes)
|
||||
.toString("utf8"),
|
||||
};
|
||||
} finally {
|
||||
stdout.destroy();
|
||||
lines.close();
|
||||
await linesClosed;
|
||||
}
|
||||
},
|
||||
|
||||
async runProcess(command, arguments_, cwd, timeout, env = process.env) {
|
||||
const result = await execute(
|
||||
command,
|
||||
arguments_,
|
||||
cwd,
|
||||
env,
|
||||
{
|
||||
timeoutMs: timeout,
|
||||
stdout: new Writable({ write: (_chunk, _encoding, done) => done() }),
|
||||
},
|
||||
`${command} timed out`,
|
||||
);
|
||||
if (result.exitCode !== 0)
|
||||
throw new Error(
|
||||
`${command} failed with ${String(result.exitCode)}: ${result.stderr.text}`,
|
||||
);
|
||||
},
|
||||
|
||||
async readCommandVersion(command) {
|
||||
const result = await execute(
|
||||
command,
|
||||
["--version"],
|
||||
repositoryRoot,
|
||||
process.env,
|
||||
{ timeoutMs: 10_000, diagnosticBytes: 8192 },
|
||||
`${command} --version timed out`,
|
||||
);
|
||||
if (result.exitCode !== 0)
|
||||
throw new Error(
|
||||
`${command} --version failed with ${String(result.exitCode)}: ${result.stderr.text}`,
|
||||
);
|
||||
const version = result.stdout.text.trim();
|
||||
if (version.length === 0)
|
||||
throw new Error(`${command} --version returned empty output`);
|
||||
return version;
|
||||
},
|
||||
|
||||
close: () => resources.close(),
|
||||
};
|
||||
}
|
||||
@@ -35,6 +35,7 @@ export const toolFamilyCatalog = (sources) => {
|
||||
...sources.evmContracts.EVM_TOOL_CONTRACTS,
|
||||
...sources.goContracts.GO_TOOL_CONTRACTS,
|
||||
...sources.il2cppContracts.IL2CPP_TOOL_CONTRACTS,
|
||||
...sources.modelContracts.MODEL_TOOL_CONTRACTS,
|
||||
...sources.rustContracts.RUST_TOOL_CONTRACTS,
|
||||
...sources.wasmContracts.WASM_TOOL_CONTRACTS,
|
||||
...sources.sqliteContracts.SQLITE_TOOL_CONTRACTS,
|
||||
@@ -271,6 +272,10 @@ export const providerCatalog = (sources) => {
|
||||
identity: sources.il2cppProvider.IL2CPP_METADATA_PROVIDER_IDENTITY,
|
||||
contracts: sources.il2cppContracts.IL2CPP_TOOL_CONTRACTS,
|
||||
},
|
||||
{
|
||||
identity: sources.modelProvider.MODEL_ARTIFACT_PROVIDER_IDENTITY,
|
||||
contracts: sources.modelContracts.MODEL_TOOL_CONTRACTS,
|
||||
},
|
||||
{
|
||||
identity: sources.rustProvider.RUST_BINARY_PROVIDER_IDENTITY,
|
||||
contracts: sources.rustContracts.RUST_TOOL_CONTRACTS,
|
||||
|
||||
@@ -69,6 +69,8 @@ const SOURCE_PATHS = {
|
||||
evmProvider: "dist/evm/EvmoleRelease.js",
|
||||
goContracts: "dist/contracts/go/goToolContracts.js",
|
||||
goProvider: "dist/go/GoBinaryProvider.js",
|
||||
modelContracts: "dist/contracts/model/modelToolContracts.js",
|
||||
modelProvider: "dist/model/ModelArtifactProvider.js",
|
||||
il2cppContracts: "dist/contracts/il2cpp/il2cppToolContracts.js",
|
||||
il2cppProvider: "dist/il2cpp/Il2cppMetadataProvider.js",
|
||||
rustContracts: "dist/contracts/rust/rustToolContracts.js",
|
||||
|
||||
@@ -14,14 +14,14 @@ export const nativeKunaIncomingSites = async ({
|
||||
binary,
|
||||
address,
|
||||
retain,
|
||||
fileCloseResources,
|
||||
resourceScope,
|
||||
}) => {
|
||||
const installed = await inspectKunaInstallation(
|
||||
{
|
||||
REA_KUNA_COMMAND: executable,
|
||||
REA_KUNA_CACHE_DIR: cacheRoot,
|
||||
},
|
||||
{ fileCloseResources },
|
||||
{ resourceScope },
|
||||
);
|
||||
assert.ok(installed.ok, "Native reference oracle could not inspect Kuna");
|
||||
const input = await realpath(binary);
|
||||
@@ -37,7 +37,7 @@ export const nativeKunaIncomingSites = async ({
|
||||
outputBytes: KUNA_XREF_OUTPUT_BYTES,
|
||||
signal: new AbortController().signal,
|
||||
retainFailedFileClose: (owner, resource) =>
|
||||
fileCloseResources.retain(owner, resource),
|
||||
resourceScope.retain(owner, resource),
|
||||
});
|
||||
if (outcome.kind === "retained") retain(outcome);
|
||||
if (outcome.kind !== "ok") throw outcome.error;
|
||||
|
||||
@@ -19,14 +19,14 @@ export const nativeKunaStrings = async ({
|
||||
workspace,
|
||||
binary,
|
||||
retain,
|
||||
fileCloseResources,
|
||||
resourceScope,
|
||||
}) => {
|
||||
const installed = await inspectKunaInstallation(
|
||||
{
|
||||
REA_KUNA_COMMAND: executable,
|
||||
REA_KUNA_CACHE_DIR: cacheRoot,
|
||||
},
|
||||
{ fileCloseResources },
|
||||
{ resourceScope },
|
||||
);
|
||||
assert.ok(installed.ok, "Native string oracle could not inspect Kuna");
|
||||
const input = await realpath(binary);
|
||||
@@ -51,7 +51,7 @@ export const nativeKunaStrings = async ({
|
||||
outputBytes: 16 * 1024 * 1024,
|
||||
signal: new AbortController().signal,
|
||||
retainFailedFileClose: (owner, resource) =>
|
||||
fileCloseResources.retain(owner, resource),
|
||||
resourceScope.retain(owner, resource),
|
||||
});
|
||||
if (outcome.kind === "retained") retain(outcome);
|
||||
if (outcome.kind !== "ok") throw outcome.error;
|
||||
@@ -87,7 +87,6 @@ export const verifyKunaStrings = async ({
|
||||
cacheRoot,
|
||||
nativeStrings,
|
||||
expectedStrings = [],
|
||||
fileCloseResources,
|
||||
}) => {
|
||||
const expected = await nativeStrings();
|
||||
if (expectedStrings.length > 0)
|
||||
|
||||
@@ -9,6 +9,7 @@ const MIGRATED_PROVIDER_ROOTS = new Set([
|
||||
"evm",
|
||||
"go",
|
||||
"il2cpp",
|
||||
"model",
|
||||
"rust",
|
||||
"wasm",
|
||||
"captures",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { ArtifactResourceScope } from "../../src/artifacts/ArtifactResourceScope.js";
|
||||
import type { FileCloseResourceOwner } from "../../src/application/AnalysisProvider.js";
|
||||
import type { RetainedResourceScope } from "../../src/application/AnalysisProvider.js";
|
||||
|
||||
/** Complete a prerequisite operation, including retained cleanup on failure. */
|
||||
export function withVerifierResourceOwner<T>(
|
||||
operation: (owner: FileCloseResourceOwner) => Promise<T>,
|
||||
operation: (owner: RetainedResourceScope) => Promise<T>,
|
||||
): Promise<T>;
|
||||
|
||||
/** Hash one verifier input and transfer failed-close ownership to its scope. */
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
chmod,
|
||||
@@ -11,7 +10,6 @@ import {
|
||||
} from "node:fs/promises";
|
||||
import { homedir } from "node:os";
|
||||
import { delimiter, dirname, join, resolve } from "node:path";
|
||||
import { createInterface } from "node:readline";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -26,6 +24,7 @@ import {
|
||||
summarizeFactualCorrectness,
|
||||
} from "../dist/evaluation/AgentEvaluationReport.js";
|
||||
import { PRODUCT_IDENTITY } from "../dist/identity.js";
|
||||
import { createAgentEvaluationCommands } from "./lib/agent-evaluation-commands.mjs";
|
||||
import { closeVerifierResources } from "./lib/verifier-cleanup.mjs";
|
||||
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
|
||||
|
||||
@@ -69,6 +68,16 @@ const evaluationEnvironment = {
|
||||
PATH: `${dirname(process.execPath)}${delimiter}${process.env.PATH ?? ""}`,
|
||||
};
|
||||
|
||||
const commands = createAgentEvaluationCommands({
|
||||
codex,
|
||||
evaluationRoot,
|
||||
evaluationEnvironment,
|
||||
optionalModel,
|
||||
timeoutMs,
|
||||
repositoryRoot,
|
||||
});
|
||||
const { runCodex, runProcess, readCommandVersion } = commands;
|
||||
|
||||
let primaryError;
|
||||
try {
|
||||
const codexVersion = await readCommandVersion(codex);
|
||||
@@ -400,10 +409,16 @@ try {
|
||||
// Even a deliberately retained fixture directory must never retain auth.
|
||||
await closeVerifierResources(
|
||||
[
|
||||
() => rm(authPath, { force: true }),
|
||||
...(process.env.REA_AGENT_EVAL_KEEP_FIXTURES === "true"
|
||||
? []
|
||||
: [() => rm(evaluationRoot, { recursive: true, force: true })]),
|
||||
async () => {
|
||||
// A failed process cleanup retains both its exact owner and its files.
|
||||
await commands.close();
|
||||
await closeVerifierResources([
|
||||
() => rm(authPath, { force: true }),
|
||||
...(process.env.REA_AGENT_EVAL_KEEP_FIXTURES === "true"
|
||||
? []
|
||||
: [() => rm(evaluationRoot, { recursive: true, force: true })]),
|
||||
]);
|
||||
},
|
||||
],
|
||||
primaryError,
|
||||
);
|
||||
@@ -487,137 +502,3 @@ function validTargetClarification(message) {
|
||||
throw cause;
|
||||
}
|
||||
}
|
||||
|
||||
async function runCodex(prompt, approvedTools) {
|
||||
const arguments_ = [
|
||||
"--no-daemon",
|
||||
"exec",
|
||||
"--ephemeral",
|
||||
"--skip-git-repo-check",
|
||||
"--sandbox",
|
||||
"read-only",
|
||||
"--json",
|
||||
"--color",
|
||||
"never",
|
||||
"-C",
|
||||
evaluationRoot,
|
||||
"-c",
|
||||
'approval_policy="never"',
|
||||
...approvedTools.flatMap((name) => [
|
||||
"-c",
|
||||
`mcp_servers.rea.tools.${name}.approval_mode="approve"`,
|
||||
]),
|
||||
...(optionalModel === undefined ? [] : ["--model", optionalModel]),
|
||||
prompt,
|
||||
];
|
||||
const child = spawn(codex, arguments_, {
|
||||
cwd: evaluationRoot,
|
||||
env: evaluationEnvironment,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
const events = [];
|
||||
const stderr = [];
|
||||
const lines = createInterface({ input: child.stdout });
|
||||
const linesClosed = new Promise((resolveClose) =>
|
||||
lines.once("close", resolveClose),
|
||||
);
|
||||
lines.on("line", (line) => {
|
||||
try {
|
||||
events.push(JSON.parse(line));
|
||||
} catch {
|
||||
stderr.push(`non-json stdout: ${line}`);
|
||||
}
|
||||
});
|
||||
child.stderr.on("data", (chunk) => {
|
||||
if (stderr.join("").length < 32_768) stderr.push(chunk.toString("utf8"));
|
||||
});
|
||||
const exitCode = await new Promise((resolveExit, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
child.kill("SIGTERM");
|
||||
reject(
|
||||
new Error(`Codex evaluation timed out after ${String(timeoutMs)}ms`),
|
||||
);
|
||||
}, timeoutMs);
|
||||
child.once("error", (cause) => {
|
||||
clearTimeout(timeout);
|
||||
reject(cause);
|
||||
});
|
||||
child.once("exit", (code) => {
|
||||
clearTimeout(timeout);
|
||||
resolveExit(code ?? 1);
|
||||
});
|
||||
});
|
||||
await linesClosed;
|
||||
return { events, exitCode, stderr: stderr.join("").slice(0, 32_768) };
|
||||
}
|
||||
|
||||
async function runProcess(
|
||||
command,
|
||||
arguments_,
|
||||
cwd,
|
||||
timeout,
|
||||
env = process.env,
|
||||
) {
|
||||
const child = spawn(command, arguments_, {
|
||||
cwd,
|
||||
env,
|
||||
stdio: ["ignore", "ignore", "pipe"],
|
||||
});
|
||||
let stderr = "";
|
||||
child.stderr.on("data", (chunk) => {
|
||||
if (stderr.length < 32_768) stderr += chunk.toString("utf8");
|
||||
});
|
||||
const code = await new Promise((resolveExit, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
child.kill("SIGTERM");
|
||||
reject(new Error(`${command} timed out`));
|
||||
}, timeout);
|
||||
child.once("error", (cause) => {
|
||||
clearTimeout(timer);
|
||||
reject(cause);
|
||||
});
|
||||
child.once("exit", (value) => {
|
||||
clearTimeout(timer);
|
||||
resolveExit(value ?? 1);
|
||||
});
|
||||
});
|
||||
if (code !== 0)
|
||||
throw new Error(`${command} failed with ${String(code)}: ${stderr}`);
|
||||
}
|
||||
|
||||
async function readCommandVersion(command) {
|
||||
const child = spawn(command, ["--version"], {
|
||||
cwd: repositoryRoot,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
child.stdout.on("data", (chunk) => {
|
||||
if (stdout.length < 4096) stdout += chunk.toString("utf8");
|
||||
});
|
||||
child.stderr.on("data", (chunk) => {
|
||||
if (stderr.length < 4096) stderr += chunk.toString("utf8");
|
||||
});
|
||||
const code = await new Promise((resolveExit, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
child.kill("SIGTERM");
|
||||
reject(new Error(`${command} --version timed out`));
|
||||
}, 10_000);
|
||||
child.once("error", (cause) => {
|
||||
clearTimeout(timer);
|
||||
reject(cause);
|
||||
});
|
||||
child.once("exit", (value) => {
|
||||
clearTimeout(timer);
|
||||
resolveExit(value ?? 1);
|
||||
});
|
||||
});
|
||||
if (code !== 0)
|
||||
throw new Error(
|
||||
`${command} --version failed with ${String(code)}: ${stderr}`,
|
||||
);
|
||||
const version = stdout.trim();
|
||||
if (version.length === 0)
|
||||
throw new Error(`${command} --version returned empty output`);
|
||||
return version;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
findMappedNonFunction,
|
||||
} from "./lib/ida-feature-trace.mjs";
|
||||
import { requireMcpToolError } from "./lib/mcp-verifier-results.mjs";
|
||||
import { withVerifierResourceOwner } from "./lib/verifier-file-resources.mjs";
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
@@ -64,9 +65,14 @@ const [
|
||||
importBuilt("ida/IdaMcpConnection.js"),
|
||||
importBuilt("ida/IdaProtocolValues.js"),
|
||||
]);
|
||||
const registration = readIdaConfiguration(process.env.REA_IDA_MCP_CONFIG);
|
||||
if (!registration.ok) throw registration.error;
|
||||
const config = registration.value;
|
||||
const config = await withVerifierResourceOwner(async (resourceScope) => {
|
||||
const registration = await readIdaConfiguration(
|
||||
process.env.REA_IDA_MCP_CONFIG,
|
||||
{ resourceScope },
|
||||
);
|
||||
if (!registration.ok) throw registration.error;
|
||||
return registration.value;
|
||||
});
|
||||
if (
|
||||
config.protocol === "native" &&
|
||||
(values["second-procedure"] === undefined ||
|
||||
|
||||
@@ -25,8 +25,9 @@ import { fileURLToPath } from "node:url";
|
||||
import { parseArgs, promisify } from "node:util";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { execFileOutput } from "../dist/process/ExecFileOutput.js";
|
||||
import { inspectKunaInstallation } from "../dist/kuna/KunaInstallation.js";
|
||||
import { ArtifactResourceScope } from "../dist/artifacts/ArtifactResourceScope.js";
|
||||
import { AnalysisCleanupLease } from "../dist/artifacts/AnalysisCleanupLease.js";
|
||||
import { requireMcpOperationResult } from "./lib/mcp-verifier-results.mjs";
|
||||
import { verifyKunaReferences } from "./lib/kuna-reference-verifier.mjs";
|
||||
import {
|
||||
@@ -71,11 +72,15 @@ if (values["compare-ghidra"]) {
|
||||
);
|
||||
}
|
||||
const run = createVerifierRun();
|
||||
const fileCloseScope = new ArtifactResourceScope();
|
||||
const fileCloseResources = {
|
||||
const inspectionResources = new ArtifactResourceScope();
|
||||
const resourceScope = {
|
||||
retain: (owner, resource) =>
|
||||
fileCloseScope.retain({ kind: "file-handle", resource, handle: owner }),
|
||||
run: (operation) => fileCloseScope.run(operation),
|
||||
inspectionResources.retain({
|
||||
kind: "lease",
|
||||
resource,
|
||||
lease: AnalysisCleanupLease.forCloseOwner(owner, resource),
|
||||
}),
|
||||
run: (operation) => inspectionResources.run(operation),
|
||||
};
|
||||
const workspace = await mkdtemp(join(tmpdir(), "rea-kuna-lane-"));
|
||||
const cacheRoot = join(workspace, "cache");
|
||||
@@ -292,7 +297,6 @@ const verifyKuna = async (
|
||||
call: cold.call,
|
||||
cacheRoot,
|
||||
expectedStrings,
|
||||
fileCloseResources,
|
||||
nativeStrings: () =>
|
||||
nativeKunaStrings({
|
||||
executable: kuna,
|
||||
@@ -300,7 +304,7 @@ const verifyKuna = async (
|
||||
workspace,
|
||||
binary: path,
|
||||
retain: (owner) => retainedNative.add(owner),
|
||||
fileCloseResources,
|
||||
resourceScope,
|
||||
}),
|
||||
}),
|
||||
});
|
||||
@@ -324,7 +328,7 @@ const verifyKuna = async (
|
||||
binary: path,
|
||||
address: targetAddress,
|
||||
retain: (outcome) => retainedNative.add(outcome),
|
||||
fileCloseResources,
|
||||
resourceScope,
|
||||
}),
|
||||
});
|
||||
const pseudocode = await cold.call("procedure_pseudo_code", {
|
||||
@@ -492,15 +496,17 @@ const verifyVariant = async ({ flavor, directory, symbols }, variant) => {
|
||||
|
||||
const results = [];
|
||||
const failures = [];
|
||||
let fileResourcesClean = true;
|
||||
let inspectionResourcesClean = true;
|
||||
try {
|
||||
kunaVersion = (
|
||||
await execFileOutput(kuna, ["--version"], {
|
||||
timeout: 10_000,
|
||||
maxBuffer: 64 * 1024,
|
||||
stopSignal: "SIGTERM",
|
||||
})
|
||||
).stdout.trim();
|
||||
const installation = await inspectKunaInstallation(
|
||||
{ REA_KUNA_COMMAND: kuna, REA_KUNA_CACHE_DIR: cacheRoot },
|
||||
{ resourceScope },
|
||||
);
|
||||
assert.ok(
|
||||
installation.ok,
|
||||
installation.ok ? undefined : installation.error.reason,
|
||||
);
|
||||
kunaVersion = `kuna ${installation.value.version}`;
|
||||
let callerPairs = [];
|
||||
if (values.targets !== undefined) {
|
||||
const names = (values.functions ?? "").split(",").filter(Boolean);
|
||||
@@ -566,14 +572,16 @@ for (const retained of [...retainedNative]) {
|
||||
}
|
||||
}
|
||||
try {
|
||||
await fileCloseScope.close();
|
||||
await inspectionResources.close();
|
||||
} catch (cause) {
|
||||
fileResourcesClean = false;
|
||||
inspectionResourcesClean = false;
|
||||
failures.push(cause);
|
||||
}
|
||||
const lifecycle = await completeVerifierRun(run);
|
||||
let cleanupComplete =
|
||||
pendingServers.size === 0 && retainedNative.size === 0 && fileResourcesClean;
|
||||
pendingServers.size === 0 &&
|
||||
retainedNative.size === 0 &&
|
||||
inspectionResourcesClean;
|
||||
try {
|
||||
assertVerifierCleanupComplete(lifecycle.process_lineage);
|
||||
} catch (cause) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { FileCloseResourceOwner } from "../application/AnalysisProvider.js";
|
||||
import type { RetainedResourceScope } from "../application/AnalysisProvider.js";
|
||||
import type {
|
||||
DoctorProviderCheck,
|
||||
DoctorProviderInspection,
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
/** Project the validated JADX configuration into the doctor contract. */
|
||||
export const inspectJadxProvider = async (
|
||||
environment: Readonly<Record<string, string | undefined>>,
|
||||
resourceOwner: FileCloseResourceOwner,
|
||||
resourceOwner: RetainedResourceScope,
|
||||
signal?: AbortSignal,
|
||||
): Promise<DoctorProviderInspection> => {
|
||||
const configured = environment.REA_JADX_MCP_JAR !== undefined;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { FileCloseResourceOwner } from "../application/AnalysisProvider.js";
|
||||
import type { RetainedResourceScope } from "../application/AnalysisProvider.js";
|
||||
import {
|
||||
AnalysisError,
|
||||
type AnalysisCapturedOutput,
|
||||
@@ -27,7 +27,7 @@ const JAVA_MODULE_LISTING_TIMEOUT_MS = 10_000;
|
||||
const JAVA_MODULE_LISTING_MAX_OUTPUT_BYTES = 1024 * 1024;
|
||||
|
||||
export interface JadxJavaProbeOptions {
|
||||
readonly resourceOwner: FileCloseResourceOwner;
|
||||
readonly resourceOwner: RetainedResourceScope;
|
||||
readonly signal?: AbortSignal | undefined;
|
||||
readonly operation?: string;
|
||||
/** The Android operation owns this deadline; the probe also has its own bound. */
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
|
||||
import { AnalysisCleanupLease } from "../artifacts/AnalysisCleanupLease.js";
|
||||
import type { FileCloseResourceOwner } from "../application/AnalysisProvider.js";
|
||||
import type { RetainedResourceScope } from "../application/AnalysisProvider.js";
|
||||
import { analysisClientFailureForOperation } from "../providers/AnalysisClientFailure.js";
|
||||
import { throwIfJadxInterrupted } from "./JadxInterruption.js";
|
||||
import { chmod, copyFile } from "node:fs/promises";
|
||||
@@ -250,7 +250,7 @@ export class JadxProvider implements AndroidAnalysisPort {
|
||||
readonly #inspections = new Set<
|
||||
Promise<Awaited<ReturnType<typeof inspectJadxAvailability>>>
|
||||
>();
|
||||
readonly #prerequisiteOwner: FileCloseResourceOwner = {
|
||||
readonly #prerequisiteOwner: RetainedResourceScope = {
|
||||
retain: (owner, resource) =>
|
||||
this.#prerequisites.retain({
|
||||
kind: "lease",
|
||||
|
||||
@@ -215,8 +215,8 @@ export interface AnalysisClientContext {
|
||||
readonly runId: string;
|
||||
}
|
||||
|
||||
/** One lifecycle owner for descriptor cleanup and abortable inspection work. */
|
||||
export interface FileCloseResourceOwner {
|
||||
/** One lifecycle owner for retained resources and abortable inspection work. */
|
||||
export interface RetainedResourceScope {
|
||||
retain(
|
||||
owner: {
|
||||
close(): Promise<void>;
|
||||
@@ -237,16 +237,25 @@ export interface ProviderIdentity {
|
||||
/** Typed analysis profile resolved before provider startup; null means unresolved. */
|
||||
export interface AnalysisProfileResolution {
|
||||
readonly profile: AnalysisProfileCommitment | null;
|
||||
/** Keep provider-private facts bound to this selection without Evidence serialization. */
|
||||
readonly createClient?: AnalysisProviderBoundClientFactory;
|
||||
}
|
||||
|
||||
export type AnalysisProviderBoundClientFactory = (
|
||||
target: BinaryTarget,
|
||||
profile: AnalysisProfileCommitment,
|
||||
context?: AnalysisClientContext,
|
||||
) => AnalysisClient;
|
||||
|
||||
/** Cancellation context for side-effect-free provider profile resolution. */
|
||||
export interface AnalysisProviderInspectionOptions {
|
||||
readonly signal?: AbortSignal;
|
||||
readonly scope?: ProviderInspectionScope;
|
||||
readonly resourceScope?: RetainedResourceScope;
|
||||
}
|
||||
|
||||
export interface AnalysisProfileResolutionOptions extends AnalysisProviderInspectionOptions {
|
||||
readonly fileCloseResources?: FileCloseResourceOwner;
|
||||
readonly resourceScope?: RetainedResourceScope;
|
||||
}
|
||||
|
||||
interface CapabilityEffects {
|
||||
@@ -279,6 +288,8 @@ export type CapabilityDescriptor = CapabilityAvailability & {
|
||||
* exclusions apply to every policy.
|
||||
*/
|
||||
readonly cachePolicy?: "snapshot" | "live";
|
||||
/** Exact source profile for an operation implemented by a versioned local reader. */
|
||||
readonly analysisProfile?: AnalysisProfileCommitment;
|
||||
readonly provider: ProviderIdentity;
|
||||
readonly operation: Exclude<AnalysisOperation, "health">;
|
||||
readonly effects: CapabilityEffects;
|
||||
@@ -317,6 +328,12 @@ export type ProviderAvailability =
|
||||
|
||||
/** Provider-owned target support observation before profile resolution. */
|
||||
export type ProviderTargetSupport =
|
||||
| {
|
||||
readonly status: "unknown";
|
||||
readonly code: null;
|
||||
readonly reason: string;
|
||||
readonly diagnostics: Readonly<Record<string, JsonValue>>;
|
||||
}
|
||||
| {
|
||||
readonly status: "supported";
|
||||
readonly code: null;
|
||||
@@ -363,6 +380,10 @@ export interface AnalysisProviderCandidate extends AnalysisProvider {
|
||||
| AnalysisProviderReadinessSnapshot
|
||||
| Promise<AnalysisProviderReadinessSnapshot>;
|
||||
inspectTargetSupport(target: BinaryTarget): ProviderTargetSupport;
|
||||
inspectTargetSupportWithOptions?(
|
||||
target: BinaryTarget,
|
||||
options?: AnalysisProviderInspectionOptions,
|
||||
): Promise<ProviderTargetSupport>;
|
||||
/** Observe retained candidate cleanup without starting or retrying work. */
|
||||
hasPendingOwnedResources?(): boolean;
|
||||
/** Release candidate resources that are independent of any active client. */
|
||||
|
||||
@@ -414,6 +414,7 @@ const workflowAvailabilityFor = ({
|
||||
name === "inspect_desktop_application" ||
|
||||
name === "inspect_go_binary" ||
|
||||
name === "inspect_il2cpp_metadata" ||
|
||||
name === "inspect_model_artifact" ||
|
||||
name === "inspect_rust_binary" ||
|
||||
name === "inspect_electron_fuses"
|
||||
)
|
||||
|
||||
@@ -23,7 +23,7 @@ import type { AnalysisProviderSelector } from "../contracts/providerSelection.js
|
||||
export type ProviderRequestBudgetResolver = (
|
||||
analysisProvider: AnalysisProviderSelector,
|
||||
environment: Readonly<Record<string, string>>,
|
||||
) => Result<number | undefined, ConfigurationError>;
|
||||
) => Promise<Result<number | undefined, ConfigurationError>>;
|
||||
|
||||
export interface ClientRegistrationEnvironmentDetails {
|
||||
readonly values: Readonly<Record<string, string>>;
|
||||
@@ -47,7 +47,6 @@ export const clientRegistrationEnvironment = (
|
||||
parsed,
|
||||
overrides,
|
||||
validation,
|
||||
undefined,
|
||||
platform,
|
||||
);
|
||||
return details.ok ? ok(details.value.values) : details;
|
||||
@@ -58,7 +57,6 @@ export const clientRegistrationEnvironmentDetails = (
|
||||
parsed: ClientConfigurationDocument | undefined,
|
||||
overrides: Readonly<Record<string, string>> = {},
|
||||
validation: "fragment" | "effective" = "effective",
|
||||
resolveProviderRequestBudget?: ProviderRequestBudgetResolver,
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
): Result<ClientRegistrationEnvironmentDetails, ConfigurationError> => {
|
||||
if (parsed?.dialect === "dsh") {
|
||||
@@ -68,12 +66,7 @@ export const clientRegistrationEnvironmentDetails = (
|
||||
platform,
|
||||
);
|
||||
if (!environment.ok) return environment;
|
||||
return validatedDetails(
|
||||
environment.value,
|
||||
validation,
|
||||
resolveProviderRequestBudget,
|
||||
platform,
|
||||
);
|
||||
return validatedDetails(environment.value, validation, platform);
|
||||
}
|
||||
const key =
|
||||
parsed?.dialect === "opencode" || parsed?.dialect === "opencode_v2"
|
||||
@@ -107,18 +100,32 @@ export const clientRegistrationEnvironmentDetails = (
|
||||
overrides,
|
||||
platform,
|
||||
);
|
||||
return validatedDetails(
|
||||
environment,
|
||||
return validatedDetails(environment, validation, platform);
|
||||
};
|
||||
|
||||
/** Resolve the async provider-specific budget through its retained reader owner. */
|
||||
export const clientRegistrationEnvironmentDetailsWithBudget = async (
|
||||
parsed: ClientConfigurationDocument | undefined,
|
||||
overrides: Readonly<Record<string, string>>,
|
||||
validation: "fragment" | "effective",
|
||||
resolveProviderRequestBudget: ProviderRequestBudgetResolver | undefined,
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
): Promise<
|
||||
Result<ClientRegistrationEnvironmentDetails, ConfigurationError>
|
||||
> => {
|
||||
const details = clientRegistrationEnvironmentDetails(
|
||||
parsed,
|
||||
overrides,
|
||||
validation,
|
||||
resolveProviderRequestBudget,
|
||||
platform,
|
||||
);
|
||||
if (!details.ok || resolveProviderRequestBudget === undefined) return details;
|
||||
return validatedBudget(details.value, resolveProviderRequestBudget, platform);
|
||||
};
|
||||
|
||||
const validatedDetails = (
|
||||
environment: Readonly<Record<string, string>>,
|
||||
validation: "fragment" | "effective",
|
||||
resolveProviderRequestBudget?: ProviderRequestBudgetResolver,
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
): Result<ClientRegistrationEnvironmentDetails, ConfigurationError> => {
|
||||
const runtimeEnvironment = snapshotEnvironment(environment, platform);
|
||||
@@ -127,12 +134,6 @@ const validatedDetails = (
|
||||
? parseEnvironmentValues(runtimeEnvironment)
|
||||
: parseEnvironment(runtimeEnvironment);
|
||||
if (!validated.ok) return validated;
|
||||
const providerRequestBudget =
|
||||
resolveProviderRequestBudget?.(
|
||||
validated.value.REA_ANALYSIS_PROVIDER,
|
||||
runtimeEnvironment,
|
||||
) ?? ok(undefined);
|
||||
if (!providerRequestBudget.ok) return providerRequestBudget;
|
||||
return ok({
|
||||
values: Object.fromEntries(
|
||||
Object.entries(environment).sort(([left], [right]) =>
|
||||
@@ -146,9 +147,29 @@ const validatedDetails = (
|
||||
...(validated.value.REA_JEB_MCP_TIMEOUT_MS === undefined
|
||||
? {}
|
||||
: { jebMcpTimeoutMs: validated.value.REA_JEB_MCP_TIMEOUT_MS }),
|
||||
...(providerRequestBudget.value === undefined
|
||||
? {}
|
||||
: { providerRequestTimeoutMs: providerRequestBudget.value }),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const validatedBudget = async (
|
||||
details: ClientRegistrationEnvironmentDetails,
|
||||
resolveProviderRequestBudget: ProviderRequestBudgetResolver,
|
||||
platform: NodeJS.Platform,
|
||||
): Promise<
|
||||
Result<ClientRegistrationEnvironmentDetails, ConfigurationError>
|
||||
> => {
|
||||
const providerBudget = await resolveProviderRequestBudget(
|
||||
details.timeoutInputs.analysisProvider,
|
||||
snapshotEnvironment(details.values, platform),
|
||||
);
|
||||
if (!providerBudget.ok) return providerBudget;
|
||||
return ok({
|
||||
...details,
|
||||
timeoutInputs: {
|
||||
...details.timeoutInputs,
|
||||
...(providerBudget.value === undefined
|
||||
? {}
|
||||
: { providerRequestTimeoutMs: providerBudget.value }),
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -37,7 +37,7 @@ import {
|
||||
import type { SetupClient } from "./SupportedClients.js";
|
||||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
import { dshDocument } from "./DshCordisPatch.js";
|
||||
import { clientRegistrationEnvironmentDetails } from "./ClientRegistrationEnvironment.js";
|
||||
import { clientRegistrationEnvironmentDetailsWithBudget } from "./ClientRegistrationEnvironment.js";
|
||||
import type { ProviderRequestBudgetResolver } from "./ClientRegistrationEnvironment.js";
|
||||
import type { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
|
||||
|
||||
@@ -262,7 +262,7 @@ export const readClientRegistrationInspections = async (
|
||||
const registration = parseRegistration(raw, effectiveClient);
|
||||
const registrationEnvironment =
|
||||
client.name === "kimi" || client.name === "codex"
|
||||
? clientRegistrationEnvironmentDetails(
|
||||
? await clientRegistrationEnvironmentDetailsWithBudget(
|
||||
parsed,
|
||||
{},
|
||||
"effective",
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
snapshotMatchesTarget,
|
||||
} from "../domain/analysisSnapshot.js";
|
||||
import type { AnalysisProfileCommitment } from "../domain/analysisProfile.js";
|
||||
import { profilelessNativeDispatchProfile } from "./binary/ProfilelessNativeWorkflowSnapshot.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import type { AnalysisSnapshot } from "../domain/analysisSnapshot.js";
|
||||
import type {
|
||||
@@ -285,7 +286,8 @@ const runAnalysis = async (
|
||||
if (!preview.ok) return cliError(preview.error);
|
||||
resolvedTarget = preview.value;
|
||||
const route = preview.value.route;
|
||||
const bindingProfile = route.profile ?? undefined;
|
||||
const bindingProfile =
|
||||
route.profile ?? nativeDispatchBindingProfile(route, tool);
|
||||
const evidenceProfile = analysisProfileForRoute(route, tool);
|
||||
// A parsed Windows path does not prove Ghidra's native snapshot owner
|
||||
// admitted the selected source; CLI replay must wait for that client.
|
||||
@@ -365,6 +367,7 @@ const runAnalysis = async (
|
||||
const workflowRecord = workflowSnapshotRecord(
|
||||
evidence,
|
||||
tool,
|
||||
evidenceProfile,
|
||||
cachePolicy,
|
||||
);
|
||||
if (workflowRecord !== undefined) {
|
||||
@@ -600,13 +603,30 @@ const analysisProfileForRoute = (
|
||||
| DirectAnalysisTool,
|
||||
): AnalysisProfileCommitment | undefined => {
|
||||
const profile = route.profile;
|
||||
if (profile === null || profile === undefined) return undefined;
|
||||
const nativeDispatchProfile =
|
||||
profile === null || profile === undefined
|
||||
? nativeDispatchBindingProfile(route, tool)
|
||||
: undefined;
|
||||
if (profile === null || profile === undefined) {
|
||||
if (nativeDispatchProfile === undefined) return undefined;
|
||||
return isWorkflowEvidenceTool(tool)
|
||||
? workflowAnalysisProfile(nativeDispatchProfile, tool)
|
||||
: nativeDispatchProfile;
|
||||
}
|
||||
if (isWorkflowEvidenceTool(tool))
|
||||
return workflowAnalysisProfile(profile, tool);
|
||||
const provider = providerIdentityForRoute(route, tool);
|
||||
return provider.id === profile.provider.id ? profile : undefined;
|
||||
};
|
||||
|
||||
const nativeDispatchBindingProfile = (
|
||||
route: SessionProviderRoute,
|
||||
operation: string,
|
||||
): AnalysisProfileCommitment | undefined => {
|
||||
if (operation !== "inspect_native_dispatch_metadata") return undefined;
|
||||
return profilelessNativeDispatchProfile(route);
|
||||
};
|
||||
|
||||
/** Mirror BinarySession.providerIdentity for a route not opened yet. */
|
||||
const providerIdentityForRoute = (
|
||||
route: SessionProviderRoute,
|
||||
@@ -623,7 +643,7 @@ const analysisProfileForEvidence = (
|
||||
| DirectAnalysisTool,
|
||||
): AnalysisProfileCommitment | undefined => {
|
||||
if (!isWorkflowEvidenceTool(tool)) return session.analysisProfile(tool);
|
||||
return session.analysisProfile();
|
||||
return session.analysisProfile() ?? session.analysisProfile(tool);
|
||||
};
|
||||
|
||||
const isWorkflowEvidenceTool = (
|
||||
|
||||
+27
-13
@@ -10,7 +10,7 @@ import { STARTUP_OWNER_CLOSED } from "../process/SharedStartupAttempt.js";
|
||||
import { constants } from "node:fs";
|
||||
import { access, readdir, stat } from "node:fs/promises";
|
||||
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
|
||||
import type { FileCloseResourceOwner } from "./AnalysisProvider.js";
|
||||
import type { RetainedResourceScope } from "./AnalysisProvider.js";
|
||||
import { homeDirectoryFromEnvironment } from "../config/homeDirectory.js";
|
||||
import { join } from "node:path";
|
||||
|
||||
@@ -480,13 +480,18 @@ export interface SystemDoctorHostOptions {
|
||||
readonly environment?: NodeJS.ProcessEnv;
|
||||
readonly execFileOutput?: typeof execFileOutput;
|
||||
readonly providerInspections?: (
|
||||
fileCloseResources: FileCloseResourceOwner,
|
||||
resourceScope: RetainedResourceScope,
|
||||
signal: AbortSignal,
|
||||
) => Promise<readonly DoctorProviderInspection[]>;
|
||||
readonly providerRegistrationEnvironment?: () => Readonly<
|
||||
Record<string, string>
|
||||
>;
|
||||
readonly resolveProviderRequestBudget?: ProviderRequestBudgetResolver;
|
||||
readonly resolveProviderRequestBudget?: (
|
||||
provider: Parameters<ProviderRequestBudgetResolver>[0],
|
||||
environment: Parameters<ProviderRequestBudgetResolver>[1],
|
||||
resources: RetainedResourceScope,
|
||||
signal: AbortSignal,
|
||||
) => ReturnType<ProviderRequestBudgetResolver>;
|
||||
readonly linuxDemoRuntimeCheck?: () => Promise<DoctorCheck>;
|
||||
}
|
||||
|
||||
@@ -506,7 +511,7 @@ export const systemDoctorHost = (
|
||||
const hostExecFileOutput = options.execFileOutput ?? execFileOutput;
|
||||
const homeDirectory = homeDirectoryFromEnvironment(environment, platform);
|
||||
const commandEnvironment = { env: environment };
|
||||
const fileCloseResources: FileCloseResourceOwner = {
|
||||
const resourceScope: RetainedResourceScope = {
|
||||
retain: (owner, resource) =>
|
||||
resources.retain({
|
||||
kind: "lease",
|
||||
@@ -515,6 +520,19 @@ export const systemDoctorHost = (
|
||||
}),
|
||||
run: (operation) => resources.run(operation),
|
||||
};
|
||||
const resolveProviderRequestBudget =
|
||||
options.resolveProviderRequestBudget === undefined
|
||||
? undefined
|
||||
: (
|
||||
provider: Parameters<ProviderRequestBudgetResolver>[0],
|
||||
environment: Parameters<ProviderRequestBudgetResolver>[1],
|
||||
) =>
|
||||
options.resolveProviderRequestBudget!(
|
||||
provider,
|
||||
environment,
|
||||
resourceScope,
|
||||
inspectionSignal,
|
||||
);
|
||||
return {
|
||||
...(options.signal === undefined ? {} : { signal: options.signal }),
|
||||
platform,
|
||||
@@ -620,10 +638,7 @@ export const systemDoctorHost = (
|
||||
: {
|
||||
providerInspections: () =>
|
||||
resources.run(() =>
|
||||
options.providerInspections!(
|
||||
fileCloseResources,
|
||||
inspectionSignal,
|
||||
),
|
||||
options.providerInspections!(resourceScope, inspectionSignal),
|
||||
),
|
||||
}),
|
||||
...(options.providerRegistrationEnvironment === undefined
|
||||
@@ -632,9 +647,9 @@ export const systemDoctorHost = (
|
||||
providerRegistrationEnvironment:
|
||||
options.providerRegistrationEnvironment,
|
||||
}),
|
||||
...(options.resolveProviderRequestBudget === undefined
|
||||
...(resolveProviderRequestBudget === undefined
|
||||
? {}
|
||||
: { resolveProviderRequestBudget: options.resolveProviderRequestBudget }),
|
||||
: { resolveProviderRequestBudget }),
|
||||
async ilspyCmdVersion(path) {
|
||||
try {
|
||||
await access(path, constants.X_OK);
|
||||
@@ -699,11 +714,10 @@ export const systemDoctorHost = (
|
||||
platform,
|
||||
environment,
|
||||
resources,
|
||||
...(options.resolveProviderRequestBudget === undefined
|
||||
...(resolveProviderRequestBudget === undefined
|
||||
? {}
|
||||
: {
|
||||
resolveProviderRequestBudget:
|
||||
options.resolveProviderRequestBudget,
|
||||
resolveProviderRequestBudget,
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -37,6 +37,7 @@ import { inspectNativeDispatch } from "./native/NativeDispatchMetadataInspection
|
||||
import { traceNativeValues } from "./native/NativeValueTrace.js";
|
||||
import { traceNativeUiAction } from "./native/NativeUiActionTrace.js";
|
||||
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
|
||||
import { addressedValueSchema } from "../domain/native/nativeInventory.js";
|
||||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
|
||||
import {
|
||||
@@ -676,11 +677,15 @@ export class EnhancedTools {
|
||||
) {
|
||||
const result = await this.#call(tool, {}, signal);
|
||||
if (!result.ok) return result;
|
||||
const parsed = z
|
||||
.array(z.object({ address: z.string(), value: z.string() }))
|
||||
.safeParse(result.value);
|
||||
const parsed = z.array(addressedValueSchema).safeParse(result.value);
|
||||
return parsed.success
|
||||
? ok(parsed.data.map(({ address, value: name }) => ({ address, name })))
|
||||
? ok(
|
||||
parsed.data.map(({ address, value: name, objc_class_label }) => ({
|
||||
address,
|
||||
name,
|
||||
...(objc_class_label === undefined ? {} : { objc_class_label }),
|
||||
})),
|
||||
)
|
||||
: err(
|
||||
new AnalysisOutputError(
|
||||
tool,
|
||||
|
||||
@@ -10,7 +10,12 @@ import type { SetupClient } from "./SupportedClients.js";
|
||||
import type { SetupHopperInstallResult } from "./SetupInstallFailure.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import { SetupSkillInstallError } from "./SetupSkill.js";
|
||||
import type { DoctorCheck, DoctorReport, DoctorScope } from "./Doctor.js";
|
||||
import type {
|
||||
DoctorCheck,
|
||||
DoctorProviderInspection,
|
||||
DoctorReport,
|
||||
DoctorScope,
|
||||
} from "./Doctor.js";
|
||||
import type { LinuxDistribution } from "./LinuxHopper.js";
|
||||
import { PRODUCT_IDENTITY, SDK_IDENTITY } from "../identity.js";
|
||||
import { CATALOG_IDENTITY } from "../catalogIdentity.js";
|
||||
@@ -52,6 +57,8 @@ export class FakeSetupHost implements SetupHost {
|
||||
configuredCommands: Array<readonly string[]> = [];
|
||||
doctorHealthy: boolean | undefined;
|
||||
scopedDoctorHealthy: boolean | undefined;
|
||||
providerInspections: readonly DoctorProviderInspection[] | undefined =
|
||||
undefined;
|
||||
linuxDemoRuntimeMissing = false;
|
||||
unsupportedHopperVersion = false;
|
||||
hopperLauncherConfigured = false;
|
||||
@@ -213,6 +220,9 @@ export class FakeSetupHost implements SetupHost {
|
||||
scope_checks: checks,
|
||||
informational_checks: [],
|
||||
...(this.hopper === undefined ? {} : { hopperPath: this.hopper }),
|
||||
...(this.providerInspections === undefined
|
||||
? {}
|
||||
: { providerInspections: this.providerInspections }),
|
||||
checks,
|
||||
identity: {
|
||||
cli_package_version: PRODUCT_IDENTITY.packageVersion,
|
||||
|
||||
@@ -584,7 +584,9 @@ const summarizeDoctor = (report: DoctorReport): SetupDoctorSummary => ({
|
||||
? ["hopper"]
|
||||
: []),
|
||||
...(report.providerInspections ?? [])
|
||||
.filter(({ available }) => available)
|
||||
.filter(
|
||||
({ available, checks }) => available && checks.every(({ ok }) => ok),
|
||||
)
|
||||
.map(({ id }) => id),
|
||||
],
|
||||
...(report.hopperPath === undefined ? {} : { hopperPath: report.hopperPath }),
|
||||
|
||||
@@ -27,7 +27,7 @@ import { isClientConfigurationCleanupFailure } from "./ClientConfigurationCleanu
|
||||
import { ClientConfigurationPublication } from "./ClientConfigurationPublication.js";
|
||||
import {
|
||||
clientRegistrationEnvironment,
|
||||
clientRegistrationEnvironmentDetails,
|
||||
clientRegistrationEnvironmentDetailsWithBudget,
|
||||
type ProviderRequestBudgetResolver,
|
||||
} from "./ClientRegistrationEnvironment.js";
|
||||
|
||||
@@ -125,7 +125,7 @@ const configureClientDocument = async (
|
||||
const policy = await readClientPolicyBlock(
|
||||
client,
|
||||
undefined,
|
||||
clientConfigurationDesired(
|
||||
await clientConfigurationDesired(
|
||||
client,
|
||||
environment,
|
||||
command,
|
||||
@@ -164,7 +164,7 @@ const configureClientDocument = async (
|
||||
const policy = await readClientPolicyBlock(
|
||||
client,
|
||||
parsed,
|
||||
clientConfigurationDesired(
|
||||
await clientConfigurationDesired(
|
||||
client,
|
||||
environment,
|
||||
command,
|
||||
@@ -190,7 +190,7 @@ const configureClientDocument = async (
|
||||
throwCleanupFailure(fallback.error);
|
||||
return { status: "failed", reason: "readback" };
|
||||
}
|
||||
const desired = clientConfigurationDesired(
|
||||
const desired = await clientConfigurationDesired(
|
||||
client,
|
||||
environment,
|
||||
command,
|
||||
@@ -408,7 +408,7 @@ export const clientConfigurationAligned = async (
|
||||
const policy = await readClientPolicyBlock(
|
||||
client,
|
||||
parsed,
|
||||
clientConfigurationDesired(
|
||||
await clientConfigurationDesired(
|
||||
client,
|
||||
providerEnvironment,
|
||||
command,
|
||||
@@ -425,7 +425,7 @@ export const clientConfigurationAligned = async (
|
||||
policy.value === undefined &&
|
||||
registrationCurrent(
|
||||
parsed,
|
||||
clientConfigurationDesired(
|
||||
await clientConfigurationDesired(
|
||||
client,
|
||||
providerEnvironment,
|
||||
command,
|
||||
@@ -497,7 +497,7 @@ const inspectClientDocument = async (
|
||||
const policy = await readClientPolicyBlock(
|
||||
client,
|
||||
undefined,
|
||||
clientConfigurationDesired(
|
||||
await clientConfigurationDesired(
|
||||
client,
|
||||
providerEnvironment,
|
||||
command,
|
||||
@@ -547,7 +547,7 @@ const inspectClientDocument = async (
|
||||
const policy = await readClientPolicyBlock(
|
||||
client,
|
||||
parsed,
|
||||
clientConfigurationDesired(
|
||||
await clientConfigurationDesired(
|
||||
client,
|
||||
providerEnvironment,
|
||||
command,
|
||||
@@ -565,7 +565,7 @@ const inspectClientDocument = async (
|
||||
}
|
||||
if (policy.value !== undefined)
|
||||
return { status: "invalid", remediation: policy.value };
|
||||
const desired = clientConfigurationDesired(
|
||||
const desired = await clientConfigurationDesired(
|
||||
client,
|
||||
providerEnvironment,
|
||||
command,
|
||||
@@ -713,14 +713,14 @@ const registrationCurrent = (
|
||||
!Object.hasOwn(parsed.legacyServers, PRODUCT_IDENTITY.mcpServerKey) &&
|
||||
!clientServerListedDisabled(parsed, PRODUCT_IDENTITY.mcpServerKey);
|
||||
|
||||
const clientConfigurationDesired = (
|
||||
const clientConfigurationDesired = async (
|
||||
client: SetupClient,
|
||||
providerEnvironment: SetupProviderEnvironment,
|
||||
command: readonly string[],
|
||||
parsed: ClientConfigurationDocument | undefined,
|
||||
sources: readonly ClientConfigurationSource[],
|
||||
resolveProviderRequestBudget?: ProviderRequestBudgetResolver,
|
||||
) => {
|
||||
): Promise<unknown> => {
|
||||
const effective = effectiveClientConfiguration(sources, {
|
||||
client,
|
||||
document: parsed,
|
||||
@@ -742,7 +742,7 @@ const clientConfigurationDesired = (
|
||||
if (!desired.ok) throw desired.error;
|
||||
return desired.value;
|
||||
}
|
||||
const validated = clientRegistrationEnvironmentDetails(
|
||||
const validated = await clientRegistrationEnvironmentDetailsWithBudget(
|
||||
effective,
|
||||
providerEnvironment,
|
||||
"effective",
|
||||
|
||||
@@ -59,6 +59,7 @@ it.each([false, true])(
|
||||
workflowSnapshotRecord(
|
||||
evidence,
|
||||
"inspect_native_api",
|
||||
ANALYSIS_SNAPSHOT_PROFILE,
|
||||
composed.value.cachePolicy,
|
||||
) === undefined,
|
||||
).toBe(transient);
|
||||
|
||||
@@ -133,12 +133,18 @@ const batchProcedureLocations = (result: JsonValue): EvidenceLocation[] => {
|
||||
export const workflowSnapshotRecord = (
|
||||
evidence: Evidence,
|
||||
operation: AnalysisOperation,
|
||||
bindingProfile: AnalysisProfileCommitment | undefined,
|
||||
cachePolicy?: "live",
|
||||
): WorkflowSnapshotRecordInput | undefined => {
|
||||
if (evidence.analysis_profile === null || cachePolicy === "live")
|
||||
if (
|
||||
evidence.analysis_profile === null ||
|
||||
bindingProfile === undefined ||
|
||||
cachePolicy === "live"
|
||||
)
|
||||
return undefined;
|
||||
return {
|
||||
operation,
|
||||
bindingProfile,
|
||||
parameters: evidence.parameters,
|
||||
execution: {
|
||||
result: evidence.normalized_result,
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
inspectAnalysisViewInputSchema,
|
||||
projectAnalysisView,
|
||||
type AnalysisViewResult,
|
||||
type AnalysisViewRequest,
|
||||
type InspectAnalysisViewInput,
|
||||
} from "../../domain/analysisView/analysisView.js";
|
||||
import { jsonValueSchema } from "../../domain/jsonValue.js";
|
||||
@@ -233,6 +234,14 @@ export const requireAnalysisViewRetention = (
|
||||
export const summarizeRetainedAnalysis = (
|
||||
evidence: Evidence,
|
||||
retention: AnalysisViewRetention,
|
||||
): Result<Evidence, AnalysisError> =>
|
||||
projectRetainedAnalysis(evidence, retention, { kind: "summary" });
|
||||
|
||||
/** Retain the complete owner observation and deliver one explicitly selected view. */
|
||||
export const projectRetainedAnalysis = (
|
||||
evidence: Evidence,
|
||||
retention: AnalysisViewRetention,
|
||||
view: AnalysisViewRequest,
|
||||
): Result<Evidence, AnalysisError> => {
|
||||
const ready = requireAnalysisViewRetention(evidence.operation, retention);
|
||||
if (!ready.ok) return ready;
|
||||
@@ -241,7 +250,7 @@ export const summarizeRetainedAnalysis = (
|
||||
return inspectAnalysisViewValidated(
|
||||
{
|
||||
source: { kind: "retained-evidence", evidence_id: evidence.evidence_id },
|
||||
view: { kind: "summary" },
|
||||
view,
|
||||
},
|
||||
ready.value.evidenceById,
|
||||
);
|
||||
|
||||
@@ -11,6 +11,7 @@ import { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import { jsonObjectSchema, type JsonValue } from "../../domain/jsonValue.js";
|
||||
import { err, ok, type Result } from "../../domain/result.js";
|
||||
import type {
|
||||
AnalysisProviderBoundClientFactory,
|
||||
AnalysisProfileResolutionOptions,
|
||||
AnalysisProviderCandidate,
|
||||
} from "../AnalysisProvider.js";
|
||||
@@ -22,6 +23,7 @@ export interface AnalysisProviderCandidateEvaluation {
|
||||
readonly candidate: AnalysisProviderCandidate;
|
||||
readonly status: AnalysisProviderCandidateStatus;
|
||||
readonly profile?: AnalysisProfileCommitment;
|
||||
readonly createClient?: AnalysisProviderBoundClientFactory;
|
||||
readonly profileError?: AnalysisError;
|
||||
}
|
||||
|
||||
@@ -107,7 +109,14 @@ export const evaluateAnalysisProviderCandidate = async (
|
||||
"Resolved analysis profile has mismatched provider identity",
|
||||
),
|
||||
);
|
||||
return ok({ candidate, status, profile });
|
||||
return ok({
|
||||
candidate,
|
||||
status,
|
||||
profile,
|
||||
...(resolved.value.createClient === undefined
|
||||
? {}
|
||||
: { createClient: resolved.value.createClient }),
|
||||
});
|
||||
} catch (cause: unknown) {
|
||||
void cause;
|
||||
return ok(
|
||||
|
||||
@@ -29,6 +29,8 @@ import {
|
||||
import { err, ok, type Result } from "../../domain/result.js";
|
||||
import type {
|
||||
AnalysisProviderCandidate,
|
||||
AnalysisProviderBoundClientFactory,
|
||||
AnalysisProviderInspectionOptions,
|
||||
CapabilityDescriptor,
|
||||
ProviderAvailability,
|
||||
ProviderIdentity,
|
||||
@@ -39,7 +41,7 @@ import {
|
||||
type AnalysisProviderCandidateEvaluation,
|
||||
} from "./AnalysisProviderEvaluation.js";
|
||||
import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js";
|
||||
import type { FileCloseResourceOwner } from "../AnalysisProvider.js";
|
||||
import type { RetainedResourceScope } from "../AnalysisProvider.js";
|
||||
|
||||
/** Deterministic discovery truth for one configured deep provider. */
|
||||
export interface AnalysisProviderCandidateStatus {
|
||||
@@ -71,6 +73,7 @@ export interface AnalysisProviderBinding {
|
||||
readonly identity: ProviderIdentity & { readonly version: string };
|
||||
readonly selectionSource: "request" | "environment" | "auto-single-candidate";
|
||||
readonly profile: AnalysisProfileCommitment;
|
||||
readonly createClient?: AnalysisProviderBoundClientFactory;
|
||||
/** The provider's capabilities, attributed to its resolved identity. */
|
||||
readonly capabilities: readonly CapabilityDescriptor[];
|
||||
}
|
||||
@@ -108,15 +111,15 @@ export class AnalysisProviderRegistry {
|
||||
>
|
||||
>();
|
||||
readonly #selectionCleanup = new Set<AnalysisError>();
|
||||
readonly #profileFiles = new ArtifactResourceScope();
|
||||
readonly #profileFileOwner: FileCloseResourceOwner = {
|
||||
readonly #profileResources = new ArtifactResourceScope();
|
||||
readonly #profileOwner: RetainedResourceScope = {
|
||||
retain: (owner, resource) =>
|
||||
this.#profileFiles.retain({
|
||||
this.#profileResources.retain({
|
||||
kind: "lease",
|
||||
resource,
|
||||
lease: AnalysisCleanupLease.forCloseOwner(owner, resource),
|
||||
}),
|
||||
run: (operation) => this.#profileFiles.run(operation),
|
||||
run: (operation) => this.#profileResources.run(operation),
|
||||
};
|
||||
#closing: Promise<Result<null, AnalysisError>> | undefined;
|
||||
|
||||
@@ -186,7 +189,7 @@ export class AnalysisProviderRegistry {
|
||||
return (
|
||||
this.#selections.size > 0 ||
|
||||
this.#discoveries.size > 0 ||
|
||||
this.#profileFiles.hasPending ||
|
||||
this.#profileResources.hasPending ||
|
||||
[...this.#selectionCleanup].some((error) => error.cleanupIncomplete) ||
|
||||
this.#providers.some(
|
||||
(provider) => provider.hasPendingOwnedResources?.() === true,
|
||||
@@ -215,7 +218,7 @@ export class AnalysisProviderRegistry {
|
||||
]);
|
||||
const failures: AnalysisError[] = [];
|
||||
try {
|
||||
await this.#profileFiles.retryCleanup();
|
||||
await this.#profileResources.retryCleanup();
|
||||
} catch (cause: unknown) {
|
||||
const cleanup = ArtifactReaderFailure.cleanupObservation(
|
||||
cause,
|
||||
@@ -269,18 +272,35 @@ export class AnalysisProviderRegistry {
|
||||
cause: options.signal.reason,
|
||||
});
|
||||
if (this.#closing !== undefined)
|
||||
return this.#providers.map((provider) =>
|
||||
projectCandidateStatus(provider, target, {
|
||||
availability: {
|
||||
status: "unknown",
|
||||
code: null,
|
||||
reason:
|
||||
"Provider ownership finalization is in progress; readiness has not been re-observed.",
|
||||
diagnostics: { finalization: "in_progress" },
|
||||
},
|
||||
capabilities: provider.capabilities(),
|
||||
}),
|
||||
);
|
||||
return this.#providers.map((provider) => ({
|
||||
provider: structuredClone(provider.identity()),
|
||||
availability: {
|
||||
status: "unknown" as const,
|
||||
code: null,
|
||||
reason:
|
||||
"Provider ownership finalization is in progress; readiness has not been re-observed.",
|
||||
diagnostics: { finalization: "in_progress" },
|
||||
},
|
||||
targetSupport:
|
||||
target === undefined
|
||||
? {
|
||||
status: "unknown" as const,
|
||||
code: null,
|
||||
reason: "No target is open.",
|
||||
diagnostics: {},
|
||||
}
|
||||
: {
|
||||
status: "unknown" as const,
|
||||
code: null,
|
||||
reason:
|
||||
"Target support has not been re-observed during provider finalization.",
|
||||
diagnostics: {},
|
||||
},
|
||||
selected: false,
|
||||
capabilities: provider
|
||||
.capabilities()
|
||||
.map((capability) => structuredClone(capability)),
|
||||
}));
|
||||
const scope = new ProviderInspectionScope();
|
||||
const discovery = new SharedStartupAttempt<
|
||||
Result<readonly AnalysisProviderCandidateStatus[], AnalysisError>
|
||||
@@ -290,6 +310,7 @@ export class AnalysisProviderRegistry {
|
||||
this.#discoverCandidates(target, {
|
||||
signal,
|
||||
scope,
|
||||
resourceScope: this.#profileOwner,
|
||||
operation: "binary_session",
|
||||
}),
|
||||
),
|
||||
@@ -318,6 +339,7 @@ export class AnalysisProviderRegistry {
|
||||
context: {
|
||||
readonly signal: AbortSignal;
|
||||
readonly scope: ProviderInspectionScope;
|
||||
readonly resourceScope: RetainedResourceScope;
|
||||
readonly operation: "binary_session" | "open_binary";
|
||||
},
|
||||
): Promise<
|
||||
@@ -453,6 +475,7 @@ export class AnalysisProviderRegistry {
|
||||
const discovery = await this.#discoverCandidates(target, {
|
||||
signal,
|
||||
scope,
|
||||
resourceScope: this.#profileOwner,
|
||||
operation: "open_binary",
|
||||
});
|
||||
if (!discovery.ok) return discovery;
|
||||
@@ -556,15 +579,17 @@ export class AnalysisProviderRegistry {
|
||||
throw new TypeError(
|
||||
"Registered provider is absent from discovery output",
|
||||
);
|
||||
if (initial.targetSupport.status !== "supported")
|
||||
if (initial.targetSupport.status === "unsupported")
|
||||
return err(selectionError("target_unsupported", providerId, baseline));
|
||||
if (initial.availability.status === "unavailable")
|
||||
return err(selectionError("provider_unavailable", providerId, baseline));
|
||||
if (initial.targetSupport.status === "unknown")
|
||||
return err(selectionError("provider_unavailable", providerId, baseline));
|
||||
const evaluation = await evaluateAnalysisProviderCandidate(
|
||||
provider,
|
||||
initial,
|
||||
target,
|
||||
{ signal, fileCloseResources: this.#profileFileOwner, scope },
|
||||
{ signal, resourceScope: this.#profileOwner, scope },
|
||||
);
|
||||
if (!evaluation.ok) return evaluation;
|
||||
const evaluated = evaluation.value;
|
||||
@@ -608,7 +633,7 @@ export class AnalysisProviderRegistry {
|
||||
status.targetSupport.status === "supported"
|
||||
? evaluateAnalysisProviderCandidate(provider, status, target, {
|
||||
signal,
|
||||
fileCloseResources: this.#profileFileOwner,
|
||||
resourceScope: this.#profileOwner,
|
||||
scope,
|
||||
})
|
||||
: ok({
|
||||
@@ -677,30 +702,44 @@ const candidateStatus = async (
|
||||
context: {
|
||||
readonly signal: AbortSignal;
|
||||
readonly scope: ProviderInspectionScope;
|
||||
readonly resourceScope: RetainedResourceScope;
|
||||
readonly operation: "binary_session" | "open_binary";
|
||||
},
|
||||
): Promise<AnalysisProviderCandidateStatus> => {
|
||||
const { signal, scope, operation } = context;
|
||||
const { signal, scope, resourceScope, operation } = context;
|
||||
if (signal.aborted)
|
||||
throw new AnalysisCancelledError(operation, { cause: signal.reason });
|
||||
const readiness =
|
||||
provider.inspectReadinessSnapshot === undefined
|
||||
? {
|
||||
availability: await provider.inspectAvailability({ signal, scope }),
|
||||
availability: await provider.inspectAvailability({
|
||||
signal,
|
||||
scope,
|
||||
resourceScope,
|
||||
}),
|
||||
capabilities: provider.capabilities(),
|
||||
}
|
||||
: await provider.inspectReadinessSnapshot({ signal, scope });
|
||||
return projectCandidateStatus(provider, target, readiness);
|
||||
: await provider.inspectReadinessSnapshot({
|
||||
signal,
|
||||
scope,
|
||||
resourceScope,
|
||||
});
|
||||
return await projectCandidateStatus(provider, target, readiness, {
|
||||
signal,
|
||||
scope,
|
||||
resourceScope,
|
||||
});
|
||||
};
|
||||
|
||||
const projectCandidateStatus = (
|
||||
const projectCandidateStatus = async (
|
||||
provider: AnalysisProviderCandidate,
|
||||
target: BinaryTarget | undefined,
|
||||
readiness: {
|
||||
readonly availability: AnalysisProviderCandidateStatus["availability"];
|
||||
readonly capabilities: readonly CapabilityDescriptor[];
|
||||
},
|
||||
): AnalysisProviderCandidateStatus => {
|
||||
options: AnalysisProviderInspectionOptions,
|
||||
): Promise<AnalysisProviderCandidateStatus> => {
|
||||
return {
|
||||
provider: structuredClone(provider.identity()),
|
||||
availability: cloneAvailability(readiness.availability),
|
||||
@@ -712,7 +751,12 @@ const projectCandidateStatus = (
|
||||
reason: "No target is open.",
|
||||
diagnostics: {},
|
||||
}
|
||||
: cloneTargetSupport(provider.inspectTargetSupport(target)),
|
||||
: cloneTargetSupport(
|
||||
await (provider.inspectTargetSupportWithOptions?.(
|
||||
target,
|
||||
options,
|
||||
) ?? provider.inspectTargetSupport(target)),
|
||||
),
|
||||
selected: false,
|
||||
capabilities: readiness.capabilities.map((capability) =>
|
||||
structuredClone(capability),
|
||||
@@ -728,6 +772,7 @@ const selectedResult = (
|
||||
source: AnalysisProviderBinding["selectionSource"],
|
||||
evaluated: AnalysisProviderCandidateEvaluation & {
|
||||
readonly profile: AnalysisProfileCommitment;
|
||||
readonly createClient?: AnalysisProviderBoundClientFactory;
|
||||
},
|
||||
statuses: readonly AnalysisProviderCandidateStatus[],
|
||||
): AnalysisProviderSelection => {
|
||||
@@ -749,6 +794,9 @@ const selectedResult = (
|
||||
identity: structuredClone(identity),
|
||||
selectionSource: source,
|
||||
profile: structuredClone(evaluated.profile),
|
||||
...(evaluated.createClient === undefined
|
||||
? {}
|
||||
: { createClient: evaluated.createClient }),
|
||||
capabilities,
|
||||
},
|
||||
candidates: replaceCandidate(statuses, selectedStatus),
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
} from "./BinarySessionExecution.js";
|
||||
import { closeAnalysisClient } from "../../providers/AnalysisClientCleanup.js";
|
||||
import { analysisErrorWithCleanupFailure } from "../../domain/analysisErrorCleanup.js";
|
||||
import { profilelessNativeDispatchProfile } from "./ProfilelessNativeWorkflowSnapshot.js";
|
||||
const OFFICIAL_OPERATIONS: ReadonlySet<string> = new Set(
|
||||
OFFICIAL_TOOL_CONTRACTS.map(({ name }) => name),
|
||||
);
|
||||
@@ -64,6 +65,7 @@ interface SessionRun {
|
||||
interface SessionBinding extends SessionRun {
|
||||
readonly target: BinaryTarget;
|
||||
readonly profile: AnalysisProfileCommitment | null;
|
||||
readonly profilelessWorkflowProfile?: AnalysisProfileCommitment;
|
||||
nativeSourceAdmitted: boolean;
|
||||
}
|
||||
|
||||
@@ -173,7 +175,12 @@ export class BinarySession
|
||||
operation?: AnalysisOperation,
|
||||
): AnalysisProfileCommitment | undefined {
|
||||
const profile = this.#active?.profile;
|
||||
if (profile === null || profile === undefined) return undefined;
|
||||
if (profile === null || profile === undefined) {
|
||||
if (operation !== "inspect_native_dispatch_metadata") return undefined;
|
||||
return this.#active?.profilelessWorkflowProfile === undefined
|
||||
? undefined
|
||||
: structuredClone(this.#active.profilelessWorkflowProfile);
|
||||
}
|
||||
if (
|
||||
operation !== undefined &&
|
||||
this.providerIdentity(operation).id !== profile.provider.id
|
||||
@@ -337,7 +344,9 @@ export class BinarySession
|
||||
return err(
|
||||
await this.#restoreAfterFailedOpen(
|
||||
previous,
|
||||
new AnalysisCancelledError("open_binary"),
|
||||
new AnalysisCancelledError("open_binary", {
|
||||
cause: options.signal?.reason,
|
||||
}),
|
||||
),
|
||||
);
|
||||
const runId = randomUUID();
|
||||
@@ -358,9 +367,14 @@ export class BinarySession
|
||||
),
|
||||
);
|
||||
}
|
||||
const profilelessWorkflowProfile =
|
||||
profile === null ? profilelessNativeDispatchProfile(route) : undefined;
|
||||
this.#active = {
|
||||
target,
|
||||
profile,
|
||||
...(profilelessWorkflowProfile === undefined
|
||||
? {}
|
||||
: { profilelessWorkflowProfile }),
|
||||
...run,
|
||||
nativeSourceAdmitted: false,
|
||||
};
|
||||
|
||||
@@ -17,6 +17,10 @@ import {
|
||||
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import { err, type Result } from "../../domain/result.js";
|
||||
import type { SessionProviderRoute } from "./SessionProviderRouter.js";
|
||||
import {
|
||||
isProfilelessNativeDispatchSnapshot,
|
||||
profilelessNativeDispatchProfile,
|
||||
} from "./ProfilelessNativeWorkflowSnapshot.js";
|
||||
import { SessionProviderRouter } from "./SessionProviderRouter.js";
|
||||
|
||||
/** Read a staged snapshot once the open holds its place in the session order. */
|
||||
@@ -126,6 +130,7 @@ export const validateResolvedSessionTarget = (
|
||||
snapshot: options.snapshot,
|
||||
target,
|
||||
profile: route.profile,
|
||||
profilelessWorkflowProfile: profilelessNativeDispatchProfile(route),
|
||||
current,
|
||||
stagedSnapshotMatches,
|
||||
});
|
||||
@@ -147,6 +152,7 @@ interface ValidateSnapshotInput {
|
||||
readonly snapshot: AnalysisSnapshot | undefined;
|
||||
readonly target: BinaryTarget;
|
||||
readonly profile: AnalysisProfileCommitment | null;
|
||||
readonly profilelessWorkflowProfile: AnalysisProfileCommitment | undefined;
|
||||
readonly current: CurrentOpenBinding | undefined;
|
||||
readonly stagedSnapshotMatches: (
|
||||
target: BinaryTarget,
|
||||
@@ -158,17 +164,31 @@ const validateSnapshot = ({
|
||||
snapshot,
|
||||
target,
|
||||
profile,
|
||||
profilelessWorkflowProfile,
|
||||
current,
|
||||
stagedSnapshotMatches,
|
||||
}: ValidateSnapshotInput): EvidenceIntegrityError | undefined => {
|
||||
if (
|
||||
snapshot !== undefined &&
|
||||
(profile === null || !snapshotMatchesBinding(snapshot, target, profile))
|
||||
(profile === null
|
||||
? profilelessWorkflowProfile === undefined ||
|
||||
!isProfilelessNativeDispatchSnapshot(
|
||||
snapshot,
|
||||
target,
|
||||
profilelessWorkflowProfile,
|
||||
)
|
||||
: !snapshotMatchesBinding(snapshot, target, profile))
|
||||
)
|
||||
return new AnalysisSnapshotMismatchError(
|
||||
"Analysis snapshot profile_mismatch: target, provider, or analysis profile does not match the requested binary",
|
||||
);
|
||||
if (current === undefined && !stagedSnapshotMatches(target, profile))
|
||||
if (
|
||||
current === undefined &&
|
||||
!stagedSnapshotMatches(
|
||||
target,
|
||||
profile ?? profilelessWorkflowProfile ?? null,
|
||||
)
|
||||
)
|
||||
return new AnalysisSnapshotMismatchError(
|
||||
"Analysis snapshot profile_mismatch: staged target, provider, or analysis profile does not match",
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { AnalysisProfileCommitment } from "../../domain/analysisProfile.js";
|
||||
import { analysisProfilesEqual } from "../../domain/analysisProfile.js";
|
||||
import type { AnalysisSnapshot } from "../../domain/analysisSnapshot.js";
|
||||
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
|
||||
import type { Evidence } from "../../domain/evidence.js";
|
||||
@@ -33,6 +34,11 @@ import { InvestigationRecords } from "../investigation/InvestigationRecords.js";
|
||||
import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
|
||||
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
|
||||
import { writeAnalysisSnapshot as writeSnapshotFile } from "./AnalysisSnapshotFiles.js";
|
||||
import { workflowAnalysisProfile } from "../InvestigationProviders.js";
|
||||
import {
|
||||
hasOnlyProfilelessNativeDispatchEntries,
|
||||
isProfilelessNativeDispatchSnapshot,
|
||||
} from "./ProfilelessNativeWorkflowSnapshot.js";
|
||||
|
||||
const SNAPSHOT_MUTATION_RECOVERY =
|
||||
"Export session observations through export_evidence_bundle if needed. Close the active target without saving a snapshot, then reopen it before importing or saving a snapshot.";
|
||||
@@ -40,11 +46,14 @@ const SNAPSHOT_MUTATION_RECOVERY =
|
||||
export interface ActiveAnalysisBinding {
|
||||
readonly target: BinaryTarget;
|
||||
readonly profile: AnalysisProfileCommitment | null;
|
||||
/** Current route profile for the single profileless native dispatch workflow. */
|
||||
readonly profilelessWorkflowProfile?: AnalysisProfileCommitment;
|
||||
}
|
||||
|
||||
/** Validated composed-workflow payload bound to the active target and profile. */
|
||||
export interface WorkflowSnapshotRecordInput {
|
||||
readonly operation: AnalysisOperation;
|
||||
readonly bindingProfile: AnalysisProfileCommitment;
|
||||
readonly parameters: Readonly<Record<string, JsonValue>>;
|
||||
readonly execution: Parameters<
|
||||
AnalysisSnapshotCache["recordWorkflow"]
|
||||
@@ -160,13 +169,30 @@ export abstract class BinarySessionRecords {
|
||||
),
|
||||
);
|
||||
const target = active?.target;
|
||||
const profile = active?.profile ?? undefined;
|
||||
const profile =
|
||||
active?.profile ?? active?.profilelessWorkflowProfile ?? undefined;
|
||||
if (target !== undefined && profile === undefined)
|
||||
return err(
|
||||
new EvidenceIntegrityError(
|
||||
"Analysis snapshots require a concrete provider analysis profile",
|
||||
),
|
||||
);
|
||||
if (
|
||||
active?.profile === null &&
|
||||
(target === undefined ||
|
||||
profile === undefined ||
|
||||
!this.#snapshot.matches(target, profile) ||
|
||||
this.#snapshot.entries().length !== 0 ||
|
||||
!hasOnlyProfilelessNativeDispatchEntries(
|
||||
this.#snapshot.workflowEntries(),
|
||||
profile,
|
||||
))
|
||||
)
|
||||
return err(
|
||||
new EvidenceIntegrityError(
|
||||
"Profileless native snapshots may contain only current dispatch workflow observations",
|
||||
),
|
||||
);
|
||||
return this.#snapshot.export(
|
||||
target,
|
||||
profile,
|
||||
@@ -192,17 +218,24 @@ export abstract class BinarySessionRecords {
|
||||
},
|
||||
),
|
||||
);
|
||||
if (active?.profile === null)
|
||||
const profile = active?.profile ?? active?.profilelessWorkflowProfile;
|
||||
if (
|
||||
active?.profile === null &&
|
||||
(profile === undefined ||
|
||||
!isProfilelessNativeDispatchSnapshot(snapshot, active.target, profile))
|
||||
)
|
||||
return err(
|
||||
new AnalysisSnapshotMismatchError(
|
||||
"Analysis snapshot profile_mismatch: the active target has no concrete analysis profile",
|
||||
"Analysis snapshot profile_mismatch: the snapshot is not bound to the active native dispatch reader",
|
||||
),
|
||||
);
|
||||
const imported = this.#snapshot.import(
|
||||
snapshot,
|
||||
active === undefined
|
||||
? undefined
|
||||
: { target: active.target, profile: active.profile },
|
||||
: profile === undefined
|
||||
? undefined
|
||||
: { target: active.target, profile },
|
||||
(bundle) => {
|
||||
const imported = this.#records.mergeEvidenceBundle(bundle);
|
||||
return imported.ok ? ok(imported.value.recordsAdded) : imported;
|
||||
@@ -252,17 +285,33 @@ export abstract class BinarySessionRecords {
|
||||
input: WorkflowSnapshotRecordInput,
|
||||
): Result<null, EvidenceIntegrityError> {
|
||||
const active = this.activeAnalysisBinding();
|
||||
if (active === undefined || active.profile === null)
|
||||
if (active === undefined)
|
||||
return err(
|
||||
new EvidenceIntegrityError(
|
||||
"Workflow snapshot entries require an active concrete provider profile",
|
||||
),
|
||||
);
|
||||
if (this.#snapshotInvalidated) return ok(null);
|
||||
const profile = active.profile ?? active.profilelessWorkflowProfile;
|
||||
if (
|
||||
profile === undefined ||
|
||||
!analysisProfilesEqual(profile, input.bindingProfile) ||
|
||||
!analysisProfilesEqual(
|
||||
input.execution.analysisProfile,
|
||||
workflowAnalysisProfile(profile, input.operation),
|
||||
) ||
|
||||
(active.profile === null &&
|
||||
input.operation !== "inspect_native_dispatch_metadata")
|
||||
)
|
||||
return err(
|
||||
new EvidenceIntegrityError(
|
||||
"Workflow snapshot entry does not match the active reader profile",
|
||||
),
|
||||
);
|
||||
try {
|
||||
this.#snapshot.recordWorkflow({
|
||||
target: active.target,
|
||||
profile: active.profile,
|
||||
profile,
|
||||
...input,
|
||||
});
|
||||
this.#emitSnapshotChanged();
|
||||
|
||||
@@ -16,6 +16,11 @@ import type {
|
||||
SessionProviderRouter,
|
||||
} from "./SessionProviderRouter.js";
|
||||
import { binarySessionStatus } from "./BinarySessionStatus.js";
|
||||
import {
|
||||
APPLE_DISPATCH_METADATA_ANALYSIS_PROFILE,
|
||||
NATIVE_MACOS_PROVIDER_IDENTITY,
|
||||
nativeMacOSCapabilities,
|
||||
} from "../../native/NativeMacOSProviderMetadata.js";
|
||||
|
||||
const projectedStatusSchema = sessionProvider.pick({
|
||||
capabilities: true,
|
||||
@@ -326,6 +331,21 @@ const expectedExpandedCandidateCapabilities = [
|
||||
];
|
||||
|
||||
describe("binary session provider capability projection", () => {
|
||||
it("keeps the private dispatch reader profile out of public capability status", async () => {
|
||||
const capabilities = nativeMacOSCapabilities("darwin");
|
||||
const projected = await project(capabilities, [
|
||||
candidate(NATIVE_MACOS_PROVIDER_IDENTITY, capabilities),
|
||||
]);
|
||||
const serialized = JSON.stringify(projected);
|
||||
expect(serialized).not.toContain(
|
||||
APPLE_DISPATCH_METADATA_ANALYSIS_PROFILE.provider.version,
|
||||
);
|
||||
expect(serialized).not.toContain(
|
||||
APPLE_DISPATCH_METADATA_ANALYSIS_PROFILE.digest,
|
||||
);
|
||||
expect(projectedStatusSchema.parse(projected)).toEqual(projected);
|
||||
});
|
||||
|
||||
it("preserves provider scope and route overlays while factoring exact prefixes", async () => {
|
||||
const selectedBefore = structuredClone(selectedCapabilities);
|
||||
const otherBefore = structuredClone(otherCapabilities);
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { AnalysisProfileCommitment } from "../../domain/analysisProfile.js"
|
||||
import type {
|
||||
AnalysisClient,
|
||||
AnalysisClientContext,
|
||||
AnalysisProviderBoundClientFactory,
|
||||
AnalysisProvider,
|
||||
CapabilityDescriptor,
|
||||
ExecutionOptions,
|
||||
@@ -129,8 +130,19 @@ export class CompositeProvider implements AnalysisProvider {
|
||||
readonly #identity: ProviderIdentity;
|
||||
readonly #capabilities: readonly CapabilityDescriptor[];
|
||||
readonly #providerByOperation: ReadonlyMap<string, AnalysisProvider>;
|
||||
readonly #boundClientFactories: ReadonlyMap<
|
||||
string,
|
||||
AnalysisProviderBoundClientFactory
|
||||
>;
|
||||
|
||||
constructor(readonly providers: readonly AnalysisProvider[]) {
|
||||
constructor(
|
||||
readonly providers: readonly AnalysisProvider[],
|
||||
boundClientFactories: ReadonlyMap<
|
||||
string,
|
||||
AnalysisProviderBoundClientFactory
|
||||
> = new Map(),
|
||||
) {
|
||||
this.#boundClientFactories = boundClientFactories;
|
||||
if (providers.length === 0)
|
||||
throw new RangeError("CompositeProvider requires at least one provider");
|
||||
this.#identity = Object.freeze(
|
||||
@@ -177,11 +189,13 @@ export class CompositeProvider implements AnalysisProvider {
|
||||
const clientFor = (provider: AnalysisProvider): AnalysisClient => {
|
||||
const existing = clients.get(provider);
|
||||
if (existing !== undefined) return existing;
|
||||
const created = provider.createClient(
|
||||
target,
|
||||
profile?.provider.id === provider.identity().id ? profile : undefined,
|
||||
context,
|
||||
);
|
||||
const providerProfile =
|
||||
profile?.provider.id === provider.identity().id ? profile : undefined;
|
||||
const factory = this.#boundClientFactories.get(provider.identity().id);
|
||||
const created =
|
||||
factory !== undefined && providerProfile !== undefined
|
||||
? factory(target, providerProfile, context)
|
||||
: provider.createClient(target, providerProfile, context);
|
||||
clients.set(provider, created);
|
||||
return created;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { analysisProfilesEqual } from "../../domain/analysisProfile.js";
|
||||
import type { AnalysisProfileCommitment } from "../../domain/analysisProfile.js";
|
||||
import {
|
||||
snapshotMatchesBinding,
|
||||
type AnalysisSnapshot,
|
||||
} from "../../domain/analysisSnapshot.js";
|
||||
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
|
||||
import { workflowAnalysisProfile } from "../InvestigationProviders.js";
|
||||
import type { SessionProviderRoute } from "./SessionProviderRouter.js";
|
||||
|
||||
/** Return the exact currently declared native dispatch reader profile, if any. */
|
||||
export const profilelessNativeDispatchProfile = (
|
||||
route: SessionProviderRoute,
|
||||
): AnalysisProfileCommitment | undefined => {
|
||||
const descriptor = route.capabilities.get("inspect_native_dispatch_metadata");
|
||||
const profile = descriptor?.analysisProfile;
|
||||
if (
|
||||
descriptor === undefined ||
|
||||
descriptor.operation !== "inspect_native_dispatch_metadata" ||
|
||||
!descriptor.available ||
|
||||
descriptor.provider.id !== "native-macos" ||
|
||||
profile === undefined ||
|
||||
profile.provider.id !== descriptor.provider.id ||
|
||||
profile.provider.name !== descriptor.provider.name ||
|
||||
profile.provider.version.length === 0
|
||||
)
|
||||
return undefined;
|
||||
return structuredClone(profile);
|
||||
};
|
||||
|
||||
/** Require only exact native dispatch workflow observations for this target/profile. */
|
||||
export const isProfilelessNativeDispatchSnapshot = (
|
||||
snapshot: AnalysisSnapshot,
|
||||
target: BinaryTarget,
|
||||
profile: AnalysisProfileCommitment,
|
||||
): boolean =>
|
||||
snapshotMatchesBinding(snapshot, target, profile) &&
|
||||
snapshot.entries.length === 0 &&
|
||||
hasOnlyProfilelessNativeDispatchEntries(snapshot.workflow_entries, profile);
|
||||
|
||||
/** Reject mixed operations and entries derived from a different source profile. */
|
||||
export const hasOnlyProfilelessNativeDispatchEntries = (
|
||||
entries: readonly {
|
||||
readonly operation: string;
|
||||
readonly execution: {
|
||||
readonly analysis_profile: AnalysisProfileCommitment;
|
||||
};
|
||||
}[],
|
||||
profile: AnalysisProfileCommitment,
|
||||
): boolean =>
|
||||
entries.length > 0 &&
|
||||
entries.every(
|
||||
(entry) =>
|
||||
entry.operation === "inspect_native_dispatch_metadata" &&
|
||||
analysisProfilesEqual(
|
||||
entry.execution.analysis_profile,
|
||||
workflowAnalysisProfile(profile, entry.operation),
|
||||
),
|
||||
);
|
||||
@@ -140,7 +140,12 @@ const selectableRoute = (
|
||||
? providers[0]
|
||||
: providers.length === 0
|
||||
? undefined
|
||||
: new CompositeProvider(providers);
|
||||
: new CompositeProvider(
|
||||
providers,
|
||||
binding?.createClient === undefined
|
||||
? new Map()
|
||||
: new Map([[binding.identity.id, binding.createClient]]),
|
||||
);
|
||||
const identity = provider?.identity() ?? emptyProviderIdentity();
|
||||
// The binding attributes deep-provider capabilities to the resolved version.
|
||||
const capabilities = capabilityMap([
|
||||
|
||||
@@ -14,7 +14,7 @@ export interface GoBinaryPort {
|
||||
readonly identity: ProviderIdentity;
|
||||
close?(): Promise<void>;
|
||||
inspect(
|
||||
input: InspectGoBinaryInput,
|
||||
input: Omit<InspectGoBinaryInput, "detail">,
|
||||
options?: ExecutionOptions,
|
||||
): Promise<Result<GoBinary, AnalysisError>>;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { GoBinaryService } from "./GoBinaryService.js";
|
||||
import {
|
||||
AnalysisCapabilityUnavailableError,
|
||||
AnalysisCancelledError,
|
||||
AnalysisInputError,
|
||||
AnalysisOutputError,
|
||||
@@ -45,6 +46,25 @@ it("rejects invalid paths and extra caller arguments before invoking the reader"
|
||||
expect(calls).toBe(0);
|
||||
});
|
||||
|
||||
it("requires a complete Evidence retention path before starting summary inspection", async () => {
|
||||
let calls = 0;
|
||||
const service = new GoBinaryService({
|
||||
identity,
|
||||
inspect: async () => {
|
||||
calls++;
|
||||
return ok(report);
|
||||
},
|
||||
});
|
||||
const result = await service.inspect({
|
||||
path: report.artifact.path,
|
||||
detail: "summary",
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok)
|
||||
expect(result.error).toBeInstanceOf(AnalysisCapabilityUnavailableError);
|
||||
expect(calls).toBe(0);
|
||||
});
|
||||
|
||||
it("preserves unsupported subject architectures and does not duplicate metadata", async () => {
|
||||
const service = new GoBinaryService({
|
||||
identity,
|
||||
|
||||
@@ -8,27 +8,24 @@ import {
|
||||
AnalysisOutputError,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import { createEvidence, type Evidence } from "../../domain/evidence.js";
|
||||
import { BINARY_ARCHITECTURES } from "../../domain/binaryTargetTypes.js";
|
||||
import {
|
||||
goArchitectureForSubject,
|
||||
goBinarySchema,
|
||||
inspectGoBinaryInputSchema,
|
||||
} from "../../domain/go/goBinary.js";
|
||||
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
|
||||
import { err, ok, type Result } from "../../domain/result.js";
|
||||
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
|
||||
import {
|
||||
requireAnalysisViewRetention,
|
||||
summarizeRetainedAnalysis,
|
||||
type AnalysisViewRetention,
|
||||
} from "../analysisView/AnalysisViewService.js";
|
||||
|
||||
const OPERATION = "inspect_go_binary";
|
||||
const architectureForSubject = (architecture: string) => {
|
||||
switch (architecture) {
|
||||
case "386":
|
||||
return "x86";
|
||||
case "amd64":
|
||||
return "x86_64";
|
||||
default:
|
||||
return BINARY_ARCHITECTURES.find(
|
||||
(supported) => supported === architecture,
|
||||
);
|
||||
}
|
||||
const NO_RETENTION: AnalysisViewRetention = {
|
||||
recordEvidence: undefined,
|
||||
evidenceById: undefined,
|
||||
};
|
||||
|
||||
/** Shared exact input validation and artifact-bound Evidence for CLI and MCP. */
|
||||
@@ -43,6 +40,7 @@ export class GoBinaryService {
|
||||
async inspect(
|
||||
rawInput: unknown,
|
||||
options?: ExecutionOptions,
|
||||
retention: AnalysisViewRetention = NO_RETENTION,
|
||||
): Promise<Result<Evidence, AnalysisError>> {
|
||||
if (options?.signal?.aborted)
|
||||
return err(
|
||||
@@ -65,7 +63,12 @@ export class GoBinaryService {
|
||||
},
|
||||
]),
|
||||
);
|
||||
const inspected = await this.provider.inspect(input.data, options);
|
||||
const { detail, ...providerInput } = input.data;
|
||||
if (detail === "summary") {
|
||||
const ready = requireAnalysisViewRetention(OPERATION, retention);
|
||||
if (!ready.ok) return ready;
|
||||
}
|
||||
const inspected = await this.provider.inspect(providerInput, options);
|
||||
if (!inspected.ok) return inspected;
|
||||
const report = goBinarySchema.safeParse(inspected.value);
|
||||
if (!report.success || report.data.artifact.path !== input.data.path)
|
||||
@@ -76,8 +79,8 @@ export class GoBinaryService {
|
||||
),
|
||||
);
|
||||
const value = report.data;
|
||||
const architecture = architectureForSubject(value.architecture);
|
||||
return completeProviderEvidence(
|
||||
const architecture = goArchitectureForSubject(value.architecture);
|
||||
const completed = completeProviderEvidence(
|
||||
createEvidence(
|
||||
{
|
||||
path: value.artifact.path,
|
||||
@@ -88,7 +91,7 @@ export class GoBinaryService {
|
||||
this.provider.identity,
|
||||
{
|
||||
operation: OPERATION,
|
||||
parameters: input.data,
|
||||
parameters: providerInput,
|
||||
result: value,
|
||||
confidence: "observed",
|
||||
limitations: value.limitations,
|
||||
@@ -108,5 +111,9 @@ export class GoBinaryService {
|
||||
),
|
||||
options?.signal,
|
||||
);
|
||||
if (!completed.ok) return completed;
|
||||
return detail === "summary"
|
||||
? summarizeRetainedAnalysis(completed.value, retention)
|
||||
: completed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { ProviderIdentity } from "../AnalysisProvider.js";
|
||||
import type {
|
||||
ModelArtifact,
|
||||
ModelArtifactInput,
|
||||
} from "../../domain/model/modelArtifact.js";
|
||||
import { createEvidence, type Evidence } from "../../domain/evidence.js";
|
||||
|
||||
/** One Evidence interpretation for success and observations retained by file finalization failures. */
|
||||
export const createModelArtifactEvidence = (
|
||||
report: ModelArtifact,
|
||||
provider: ProviderIdentity,
|
||||
parameters: ModelArtifactInput,
|
||||
): Evidence => {
|
||||
const source = report.artifact;
|
||||
return createEvidence(
|
||||
source.sha256 === null
|
||||
? undefined
|
||||
: { path: source.path, sha256: source.sha256, format: "file" },
|
||||
provider,
|
||||
{
|
||||
operation: "inspect_model_artifact",
|
||||
parameters: { path: parameters.path, hash: parameters.hash },
|
||||
result: report,
|
||||
confidence: "observed",
|
||||
subjectUnavailableReason:
|
||||
"Full-file identity is unknown in header-only mode; normalized_result.artifact records a scoped header SHA-256 and exact source path/extent.",
|
||||
locations: [
|
||||
{ kind: "artifact-path", path: source.path },
|
||||
{ kind: "file-offset-range", start: 0, end: source.header_length },
|
||||
],
|
||||
limitations: report.limitations,
|
||||
},
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import type {
|
||||
ExecutionOptions,
|
||||
ProviderIdentity,
|
||||
} from "../AnalysisProvider.js";
|
||||
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import type {
|
||||
ModelArtifact,
|
||||
ModelArtifactInput,
|
||||
} from "../../domain/model/modelArtifact.js";
|
||||
import type { Result } from "../../domain/result.js";
|
||||
|
||||
/** Read-only, local model declarations; no model runtime, target execution or downloads. */
|
||||
export interface ModelArtifactPort {
|
||||
readonly identity: ProviderIdentity;
|
||||
inspect(
|
||||
input: ModelArtifactInput,
|
||||
options?: ExecutionOptions,
|
||||
): Promise<Result<ModelArtifact, AnalysisError>>;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { inspectModelArtifactInputSchema } from "../../domain/model/modelArtifactQuery.js";
|
||||
import type { ModelArtifactPort } from "./ModelArtifactPort.js";
|
||||
import type { ExecutionOptions } from "../AnalysisProvider.js";
|
||||
import { modelArtifactSchema } from "../../domain/model/modelArtifact.js";
|
||||
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
|
||||
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisOutputError,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import type { Evidence } from "../../domain/evidence.js";
|
||||
import { createModelArtifactEvidence } from "./ModelArtifactEvidence.js";
|
||||
import { err, ok, type Result } from "../../domain/result.js";
|
||||
import {
|
||||
requireAnalysisViewRetention,
|
||||
projectRetainedAnalysis,
|
||||
type AnalysisViewRetention,
|
||||
} from "../analysisView/AnalysisViewService.js";
|
||||
|
||||
/** Shared input, Evidence and retained delivery boundary for CLI, MCP and SDK callers. */
|
||||
export class ModelArtifactService {
|
||||
constructor(readonly provider: ModelArtifactPort) {}
|
||||
close(): Promise<void> {
|
||||
return this.provider.close();
|
||||
}
|
||||
async inspect(
|
||||
raw: unknown,
|
||||
options?: ExecutionOptions,
|
||||
retention: AnalysisViewRetention = {
|
||||
evidenceById: undefined,
|
||||
recordEvidence: undefined,
|
||||
},
|
||||
): Promise<Result<Evidence, AnalysisError>> {
|
||||
const input = inspectModelArtifactInputSchema.safeParse(raw);
|
||||
if (!input.success)
|
||||
return err(
|
||||
analysisInputErrorFromIssues(
|
||||
"inspect_model_artifact",
|
||||
input.error.issues,
|
||||
raw,
|
||||
{ cause: input.error },
|
||||
),
|
||||
);
|
||||
if (options?.signal?.aborted)
|
||||
return err(
|
||||
new AnalysisCancelledError("inspect_model_artifact", {
|
||||
cause: options.signal.reason,
|
||||
}),
|
||||
);
|
||||
const { detail, view, ...parameters } = input.data;
|
||||
if (detail === "summary") {
|
||||
const ready = requireAnalysisViewRetention(
|
||||
"inspect_model_artifact",
|
||||
retention,
|
||||
);
|
||||
if (!ready.ok) return ready;
|
||||
}
|
||||
const inspected = await this.provider.inspect(parameters, options);
|
||||
if (!inspected.ok) return inspected;
|
||||
const checked = modelArtifactSchema.safeParse(inspected.value);
|
||||
if (
|
||||
!checked.success ||
|
||||
checked.data.artifact.path !== parameters.path ||
|
||||
checked.data.artifact.read_scope !==
|
||||
(parameters.hash === "full" ? "full-file" : "header") ||
|
||||
(parameters.hash === "header"
|
||||
? checked.data.artifact.sha256 !== null
|
||||
: checked.data.artifact.sha256 === null)
|
||||
)
|
||||
return err(
|
||||
new AnalysisOutputError(
|
||||
"inspect_model_artifact",
|
||||
"Model reader changed the selected input or returned invalid facts",
|
||||
),
|
||||
);
|
||||
const report = checked.data;
|
||||
const evidence = createModelArtifactEvidence(
|
||||
report,
|
||||
this.provider.identity,
|
||||
parameters,
|
||||
);
|
||||
if (options?.signal?.aborted)
|
||||
return err(
|
||||
new AnalysisCancelledError("inspect_model_artifact", {
|
||||
cause: options.signal.reason,
|
||||
partialObservation: evidence,
|
||||
}),
|
||||
);
|
||||
return detail === "summary"
|
||||
? projectRetainedAnalysis(
|
||||
evidence,
|
||||
retention,
|
||||
view === undefined
|
||||
? { kind: "summary" }
|
||||
: { kind: "model-artifact", ...view },
|
||||
)
|
||||
: ok(evidence);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { parseBinary } from "plist";
|
||||
import { appleResourceKeyFields } from "../../domain/apple/appleAssetCatalog.js";
|
||||
import {
|
||||
interfaceBuilderConnectionFieldNames,
|
||||
isInterfaceBuilderConnectionClass,
|
||||
isUIKitRuntimeConnectionClass,
|
||||
} from "../../domain/apple/interfaceBuilderKeyedArchive.js";
|
||||
import { DirectoryArtifactReader } from "../DirectoryArtifactReader.js";
|
||||
@@ -579,7 +580,7 @@ const projectNibArchive = (
|
||||
// AppKit connectors use NS-prefixed keys; UIKit's runtime outlet and
|
||||
// event connections use UI-prefixed keys with the same roles.
|
||||
const uiKit = isUIKitRuntimeConnectionClass(className);
|
||||
if (!uiKit && !className.includes("Connector")) continue;
|
||||
if (!isInterfaceBuilderConnectionClass(className)) continue;
|
||||
const values = object.values;
|
||||
const connectionFields = interfaceBuilderConnectionFieldNames(className);
|
||||
const source = reference(values[connectionFields.source]);
|
||||
|
||||
@@ -77,6 +77,23 @@ describe("NIBArchive decoder", () => {
|
||||
expect(decoded.coder_version).toBe(10);
|
||||
});
|
||||
|
||||
it("counts AppKit outlet, action, and binding connector objects", () => {
|
||||
const archive = encodeArchive({
|
||||
classes: [
|
||||
"NSNibOutletConnector",
|
||||
"NSNibControlConnector",
|
||||
"NSNibBindingConnector",
|
||||
],
|
||||
objects: [
|
||||
{ classIndex: 0, values: {} },
|
||||
{ classIndex: 1, values: {} },
|
||||
{ classIndex: 2, values: {} },
|
||||
],
|
||||
});
|
||||
|
||||
expect(decodeNibArchive(archive).connection_object_count).toBe(3);
|
||||
});
|
||||
|
||||
it("rejects malformed references and unsupported format versions", () => {
|
||||
const valid = encodeArchive({
|
||||
classes: ["NSObject"],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { JsonValue } from "../../domain/jsonValue.js";
|
||||
import { hasNibArchiveSignature } from "../../domain/apple/appleArchiveFormat.js";
|
||||
import { isInterfaceBuilderConnectionClass } from "../../domain/apple/interfaceBuilderKeyedArchive.js";
|
||||
import { TextDecoder } from "node:util";
|
||||
import {
|
||||
InterfaceBuilderDecodeBudget,
|
||||
@@ -147,9 +148,7 @@ export const decodeNibArchive = (
|
||||
});
|
||||
let connectionObjectCount = 0;
|
||||
for (const object of objects)
|
||||
if (
|
||||
/(?:Outlet|Connection|Segue|ActionConnection)/iu.test(object.class_name)
|
||||
)
|
||||
if (isInterfaceBuilderConnectionClass(object.class_name))
|
||||
connectionObjectCount += 1;
|
||||
|
||||
return {
|
||||
|
||||
@@ -11,6 +11,7 @@ import { CdpCaptureCompleteness } from "./CdpCaptureCompleteness.js";
|
||||
import { authorizedMainFrame } from "./CdpAuthorizedMainFrame.js";
|
||||
import {
|
||||
allowedSanitizedUrl,
|
||||
delayWithCancellation,
|
||||
isHttpUrl,
|
||||
numberValue,
|
||||
recordValue,
|
||||
@@ -356,32 +357,38 @@ const waitForWindow = async (
|
||||
signal?: AbortSignal,
|
||||
): Promise<EndReason> =>
|
||||
await new Promise<EndReason>((resolve, reject) => {
|
||||
const timerController = new AbortController();
|
||||
let settled = false;
|
||||
let removeEnd = (): void => {};
|
||||
const finish = (reason: EndReason): void => {
|
||||
const settle = (complete: () => void): void => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
timerController.abort();
|
||||
signal?.removeEventListener("abort", onAbort);
|
||||
removeEnd();
|
||||
resolve(reason);
|
||||
complete();
|
||||
};
|
||||
const onAbort = (): void => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
signal?.removeEventListener("abort", onAbort);
|
||||
removeEnd();
|
||||
reject(
|
||||
new AnalysisCancelledError("observe_web_session", {
|
||||
cause: signal?.reason,
|
||||
}),
|
||||
);
|
||||
};
|
||||
const timer = setTimeout(() => finish("window_elapsed"), durationMs);
|
||||
removeEnd = capture.onEnd(finish);
|
||||
const finish = (reason: EndReason): void => settle(() => resolve(reason));
|
||||
const onAbort = (): void =>
|
||||
settle(() => {
|
||||
reject(
|
||||
new AnalysisCancelledError("observe_web_session", {
|
||||
cause: signal?.reason,
|
||||
}),
|
||||
);
|
||||
});
|
||||
signal?.addEventListener("abort", onAbort, { once: true });
|
||||
if (signal?.aborted === true) onAbort();
|
||||
if (!settled) removeEnd = capture.onEnd(finish);
|
||||
if (settled) return;
|
||||
void delayWithCancellation(
|
||||
durationMs,
|
||||
"observe_web_session",
|
||||
timerController.signal,
|
||||
).then(
|
||||
() => finish("window_elapsed"),
|
||||
(cause: unknown) => settle(() => reject(cause)),
|
||||
);
|
||||
});
|
||||
|
||||
const captureFinalUrl = async (
|
||||
|
||||
@@ -7,6 +7,7 @@ import { createDirectAnalysis } from "./composition/directAnalysis.js";
|
||||
import { registerEvmCommands } from "./cli/evmCommands.js";
|
||||
import { registerDesktopApplicationCommands } from "./cli/desktopApplicationCommands.js";
|
||||
import { registerGoCommands } from "./cli/goCommands.js";
|
||||
import { registerModelCommands } from "./cli/modelCommands.js";
|
||||
import { registerIl2cppCommands } from "./cli/il2cppCommands.js";
|
||||
import { registerRustCommands } from "./cli/rustCommands.js";
|
||||
import { registerSqliteCommands } from "./cli/sqliteCommands.js";
|
||||
@@ -122,6 +123,7 @@ export const createCli = (
|
||||
registerDesktopApplicationCommands(cli, logger);
|
||||
registerGoCommands(cli, logger);
|
||||
registerIl2cppCommands(cli, logger);
|
||||
registerModelCommands(cli, logger);
|
||||
registerRustCommands(cli, logger);
|
||||
registerSqliteCommands(cli, logger, environment);
|
||||
registerArtifactCommands(cli, logger, analysis.runProviderAnalysis);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AnalysisViewRequest } from "../domain/analysisView/analysisView.js";
|
||||
import { z } from "incur";
|
||||
import { parseEvidence, type Evidence } from "../domain/evidence.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
@@ -34,6 +35,7 @@ const isEvidence = (value: JsonValue): value is Evidence =>
|
||||
"normalized_result" in value;
|
||||
|
||||
interface CliEvidenceDelivery {
|
||||
readonly view?: AnalysisViewRequest;
|
||||
readonly providerId: string;
|
||||
readonly operation: string;
|
||||
readonly signal: AbortSignal;
|
||||
@@ -95,6 +97,7 @@ export const deliverCliAnalysisEvidence = async (
|
||||
signal,
|
||||
detail,
|
||||
evidenceOutput,
|
||||
...(input.view === undefined ? {} : { view: input.view }),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -139,7 +142,7 @@ export const publishCliAnalysisEvidence = async (
|
||||
return detail === "summary"
|
||||
? inspectAnalysisView({
|
||||
source: { kind: "inline", evidence: complete },
|
||||
view: { kind: "summary" },
|
||||
view: input.view ?? { kind: "summary" },
|
||||
})
|
||||
: { ok: true as const, value: complete };
|
||||
},
|
||||
|
||||
@@ -71,7 +71,7 @@ export const registerAnalysisViewCommands = (
|
||||
});
|
||||
cli.command(CLI_COMMANDS.inspectAnalysisView, {
|
||||
description:
|
||||
"Project a selected view of completed binary-layout, JavaScript application, native function/inventory, artifact-inventory, desktop-directory, WASM, Mach-O, Interface Builder, EVM, managed, Android package, Dart AOT, web network capture or pcap Evidence JSON",
|
||||
"Project a selected view of completed Go build metadata, binary-layout, JavaScript application, native function/inventory, artifact-inventory, desktop-directory, WASM, Mach-O, Interface Builder, EVM, managed, Android package, Dart AOT, web network capture or pcap Evidence JSON",
|
||||
args: z.object({
|
||||
inputJson: z.string().describe("Inline workflow JSON or JSON file path"),
|
||||
}),
|
||||
|
||||
+13
-7
@@ -5,7 +5,10 @@ import { logCliCommand } from "../cliLogging.js";
|
||||
import type { Logger } from "pino";
|
||||
import type { CliInstance } from "./types.js";
|
||||
import { withCommandCancellation } from "./commandCancellation.js";
|
||||
import { finishAnalysisCommand } from "./finishAnalysisCommand.js";
|
||||
import {
|
||||
analysisEvidenceDeliveryOptions,
|
||||
deliverCliAnalysisEvidence,
|
||||
} from "./analysisEvidenceDelivery.js";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
|
||||
/** Resolve caller paths and inspect static build metadata through the shared workflow. */
|
||||
@@ -16,24 +19,27 @@ export const registerGoCommands = (cli: CliInstance, logger: Logger): void => {
|
||||
args: z.object({
|
||||
path: z.string().describe("Local ELF, PE or thin Mach-O image"),
|
||||
}),
|
||||
run: ({ args }) =>
|
||||
options: z.object(analysisEvidenceDeliveryOptions),
|
||||
run: ({ args, options }) =>
|
||||
withCommandCancellation(async (signal) => {
|
||||
const service = createGoBinaryService();
|
||||
return await logCliCommand(
|
||||
logger,
|
||||
CLI_COMMANDS.inspectGoBinary,
|
||||
async () =>
|
||||
finishAnalysisCommand(
|
||||
() =>
|
||||
deliverCliAnalysisEvidence({
|
||||
run: () =>
|
||||
service.inspect(
|
||||
{ path: absoluteCallerPath(args.path) },
|
||||
{ signal },
|
||||
),
|
||||
service,
|
||||
"go-build-info",
|
||||
"inspect_go_binary",
|
||||
providerId: "go-build-info",
|
||||
operation: "inspect_go_binary",
|
||||
signal,
|
||||
),
|
||||
detail: options.detail,
|
||||
evidenceOutput: options.evidence_output,
|
||||
}),
|
||||
);
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import { inspectModelArtifactInputSchema } from "../domain/model/modelArtifactQuery.js";
|
||||
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
|
||||
import { AnalysisInputError } from "../domain/analysisErrorCore.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import { z } from "incur";
|
||||
import type { Logger } from "pino";
|
||||
import type { CliInstance } from "./types.js";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
import { createModelArtifactService } from "../composition/model.js";
|
||||
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
||||
import { logCliCommand } from "../cliLogging.js";
|
||||
import { withCommandCancellation } from "./commandCancellation.js";
|
||||
import {
|
||||
analysisEvidenceDeliveryOptions,
|
||||
deliverCliAnalysisEvidence,
|
||||
} from "./analysisEvidenceDelivery.js";
|
||||
|
||||
/** One-shot summaries export the same complete observations used by MCP retained pages. */
|
||||
export const registerModelCommands = (
|
||||
cli: CliInstance,
|
||||
logger: Logger,
|
||||
): void => {
|
||||
cli.command(CLI_COMMANDS.inspectModelArtifact, {
|
||||
description:
|
||||
"Inspect GGUF or safetensors headers without loading model weights",
|
||||
args: z.object({
|
||||
path: z.string().describe("Local model file, detected by content"),
|
||||
}),
|
||||
options: z.object({
|
||||
...analysisEvidenceDeliveryOptions,
|
||||
view: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"JSON model page selection; requires --detail summary and --evidence_output",
|
||||
),
|
||||
hash: z
|
||||
.enum(["header", "full"])
|
||||
.default("header")
|
||||
.describe(
|
||||
"header reads declarations only; full additionally streams all bytes for SHA-256",
|
||||
),
|
||||
}),
|
||||
run: ({ args, options }) =>
|
||||
withCommandCancellation((signal) =>
|
||||
logCliCommand(logger, CLI_COMMANDS.inspectModelArtifact, async () => {
|
||||
let view: unknown;
|
||||
try {
|
||||
view =
|
||||
options.view === undefined ? undefined : JSON.parse(options.view);
|
||||
} catch (cause) {
|
||||
return projectAnalysisError(
|
||||
new AnalysisInputError("inspect_model_artifact", { cause }, [
|
||||
{
|
||||
path: ["view"],
|
||||
reason: "invalid_format",
|
||||
message: "view must be a JSON model page selection",
|
||||
},
|
||||
]),
|
||||
);
|
||||
}
|
||||
const selected = inspectModelArtifactInputSchema.safeParse({
|
||||
path: absoluteCallerPath(args.path),
|
||||
hash: options.hash,
|
||||
detail: options.detail,
|
||||
...(view === undefined ? {} : { view }),
|
||||
});
|
||||
if (!selected.success)
|
||||
return projectAnalysisError(
|
||||
analysisInputErrorFromIssues(
|
||||
"inspect_model_artifact",
|
||||
selected.error.issues,
|
||||
{ view },
|
||||
),
|
||||
);
|
||||
const service = createModelArtifactService();
|
||||
return deliverCliAnalysisEvidence({
|
||||
run: () =>
|
||||
service.inspect(
|
||||
{
|
||||
path: absoluteCallerPath(args.path),
|
||||
hash: options.hash,
|
||||
detail: "complete",
|
||||
},
|
||||
{ signal },
|
||||
),
|
||||
service,
|
||||
providerId: "model-artifact",
|
||||
operation: "inspect_model_artifact",
|
||||
signal,
|
||||
detail: options.detail,
|
||||
evidenceOutput: options.evidence_output,
|
||||
...(selected.data.view === undefined
|
||||
? {}
|
||||
: {
|
||||
view: {
|
||||
kind: "model-artifact" as const,
|
||||
...selected.data.view,
|
||||
},
|
||||
}),
|
||||
});
|
||||
}),
|
||||
),
|
||||
});
|
||||
};
|
||||
@@ -61,6 +61,7 @@ export const CLI_COMMANDS = Object.freeze({
|
||||
inspectDesktopApplication: "inspect-desktop-application",
|
||||
inspectGoBinary: "inspect-go-binary",
|
||||
inspectIl2cppMetadata: "inspect-il2cpp-metadata",
|
||||
inspectModelArtifact: "inspect-model-artifact",
|
||||
inspectRustBinary: "inspect-rust-binary",
|
||||
inspectPeResources: "inspect-pe-resources",
|
||||
inspectAnalysisView: "inspect-analysis-view",
|
||||
|
||||
@@ -74,6 +74,8 @@ describe("CLI JSON path normalization", () => {
|
||||
expect(parsed.error.issues).toEqual([
|
||||
expect.objectContaining({
|
||||
path: ["executable_path"],
|
||||
code: "invalid_format",
|
||||
message: expect.stringContaining("absolute local filesystem path"),
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { ModelArtifactService } from "../application/model/ModelArtifactService.js";
|
||||
import { ModelArtifactProvider } from "../model/ModelArtifactProvider.js";
|
||||
|
||||
/** Construct the local reader without acquiring files, processes or network resources. */
|
||||
export const createModelArtifactService = (): ModelArtifactService =>
|
||||
new ModelArtifactService(new ModelArtifactProvider());
|
||||
@@ -17,7 +17,7 @@ export const ANALYSIS_VIEW_TOOL_CONTRACTS = [
|
||||
...toolContractMetadata("inspect_analysis_view"),
|
||||
kind: "application",
|
||||
description:
|
||||
'Inspect selected views of completed binary-layout, JavaScript application, native analyze_function, inspect_artifact, inventory_artifact, inspect_wasm_artifact, inspect_macho, decode_interface_builder, inspect_evm_interface, inspect_managed_artifact, inspect_managed_members, inspect_android_package, inspect_dart_aot, inspect_il2cpp_metadata or inspect_web_network_capture Evidence without repeating analysis. Historical capture summaries return identity and counts without bodies or archive inventories. Capture views page retained records: view {kind: "capture", detail: "preview" | "complete", offset: 0, limit: 20}; preview selects recorded headline fields and binary descriptors, complete returns exact records with all sidecars. Offsets follow the selected parent record order, preserving original producer ordinals. Paging never reopens the source or reruns the decoder. For inspect_pcap_capture parents, kind pcap pages endpoints, dns-queries, tls-sni, protocols, http-uris or unattributed-addresses. Select an exact query for dns-addresses, or a tshark/capinfos stream for UTF-16 diagnostics text. Page totals count retained facts; source coverage remains explicit. Qlog summaries keep profile/scope and counts; kind qlog pages traces, streams, loss-declarations, incomplete-records, contributions, ranges or final-sizes. Except traces, select trace_index; stream-specific facets also require exact stream_id and direction. Facts retain all-recorded-events-before-selection scope, and interval overlap or endpoint loss declarations do not prove retransmission or network loss. The api-surface view groups retained HAR requests by method, origin and format-rule path template; templated segments are inferences. It reads no headers or bodies and reports no query values. The environment-manifest view projects the same HAR requests on the primary origin (most JSON-like records) into a replayable tool surface: view {kind: "environment-manifest", template_rules: ["after-uid-literal", "opaque-token", "sibling-variance"], include_query_parameter_names: true}. Each tool has a deterministic name, a path template and parameters from the four format rules plus the selected opt-in rules, observed query parameter names, a value-free body shape for POST/PUT/PATCH, status and content-type sets and record ordinals; records are pointers into the parent, excluded groups carry a reason and three fixed unknowns are always stated. Templates, parameters and names are inferences; the view copies no header, cookie or body values and refuses parents inspected with sensitive values. A capture_browser_scenario parent that selected response bodies or header values projects the same way, one record per network transaction in request order; it relies on capture-time redaction of declared secrets instead of that refusal, and a transaction whose URL was redacted is left unrepresented. The environment-state view extracts, from the same parents, the initial state the recorded reads observed: view {kind: "environment-state", template_rules: [...]} with the rules of the manifest it accompanies. It reads response bodies recorded before the first related write and reports entity types with an identity field (proved by a path parameter or inferred from a field name), entities in collection order with per-view values where views differ, documents for non-parameterized non-entity paths written by POST/PUT/PATCH (item-shaped and delete-only paths stay recorded), and a render plan per read tool: item and list views reproduce their latest recordings from the state, and every other read tool is a recorded view with a reason. Types and identity are inferences; values are recorded data, so it refuses parents inspected with sensitive values, and a scenario parent must have selected response bodies; its values keep the capture redaction markers, and a write whose URL it redacted ends the state of every type. For a retained record use source: {kind: "retained-evidence", evidence_id: "<parent_evidence_id>"}. Use normalized_result.parent_evidence_id from a summary receipt. Source kind is required. Portable source uses {kind: "inline", evidence: <complete Evidence>}. A completed Evidence JSON file on this host uses {kind: "evidence-file", path: "<absolute path>"}: REA reads and authenticates it, then projects it like retained Evidence, so a large capture is bounded by the file reader rather than by inline-input accounting. For layout, JavaScript, inventory, WASM, Mach-O, Interface Builder, EVM, managed artifact, Android package and Dart AOT sources, request a summary with view: {kind: "summary"}; summary is a view kind and has no facet. The WASM summary gives validation, counts, WAT digest/length, tool profile and execution/network states without reading WAT text. Select a layout mitigations/linkage facet, one section/symbol/module, or a stable page. Module pages include assets, bundled and source modules; use node_id for ambiguous or unavailable paths. Typed module refeLine truncated
|
||||
'Inspect selected views of completed Go build metadata, binary-layout, JavaScript application, native analyze_function, inspect_artifact, inventory_artifact, inspect_wasm_artifact, inspect_macho, decode_interface_builder, inspect_evm_interface, inspect_managed_artifact, inspect_managed_members, inspect_android_package, inspect_dart_aot, inspect_il2cpp_metadata, inspect_model_artifact or inspect_web_network_capture Evidence without repeating analysis. Historical capture summaries return identity and counts without bodies or archive inventories. Capture views page retained records: view {kind: "capture", detail: "preview" | "complete", offset: 0, limit: 20}; preview selects recorded headline fields and binary descriptors, complete returns exact records with all sidecars. Offsets follow the selected parent record order, preserving original producer ordinals. Paging never reopens the source or reruns the decoder. Go metadata uses kind go, collection dependencies/settings, exact paths/keys or indices, fields and pagination. Row index refers to the original collection; empty selections return no rows, and dependency coverage remains unknown. For inspect_pcap_capture parents, kind pcap pages endpoints, dns-queries, tls-sni, protocols, http-uris or unattributed-addresses. Select an exact query for dns-addresses, or a tshark/capinfos stream for UTF-16 diagnostics text. Page totals count retained facts; source coverage remains explicit. Qlog summaries keep profile/scope and counts; kind qlog pages traces, streams, loss-declarations, incomplete-records, contributions, ranges or final-sizes. Except traces, select trace_index; stream-specific facets also require exact stream_id and direction. Facts retain all-recorded-events-before-selection scope, and interval overlap or endpoint loss declarations do not prove retransmission or network loss. The api-surface view groups retained HAR requests by method, origin and format-rule path template; templated segments are inferences. It reads no headers or bodies and reports no query values. The environment-manifest view projects the same HAR requests on the primary origin (most JSON-like records) into a replayable tool surface: view {kind: "environment-manifest", template_rules: ["after-uid-literal", "opaque-token", "sibling-variance"], include_query_parameter_names: true}. Each tool has a deterministic name, a path template and parameters from the four format rules plus the selected opt-in rules, observed query parameter names, a value-free body shape for POST/PUT/PATCH, status and content-type sets and record ordinals; records are pointers into the parent, excluded groups carry a reason and three fixed unknowns are always stated. Templates, parameters and names are inferences; the view copies no header, cookie or body values and refuses parents inspected with sensitive values. A capture_browser_scenario parent that selected response bodies or header values projects the same way, one record per network transaction in request order; it relies on capture-time redaction of declared secrets instead of that refusal, and a transaction whose URL was redacted is left unrepresented. The environment-state view extracts, from the same parents, the initial state the recorded reads observed: view {kind: "environment-state", template_rules: [...]} with the rules of the manifest it accompanies. It reads response bodies recorded before the first related write and reports entity types with an identity field (proved by a path parameter or inferred from a field name), entities in collection order with per-view values where views differ, documents for non-parameterized non-entity paths written by POST/PUT/PATCH (item-shaped and delete-only paths stay recorded), and a render plan per read tool: item and list views reproduce their latest recordings from the state, and every other read tool is a recorded view with a reason. Types and identity are inferences; values are recorded data, so it refuses parents inspected with sensitive values, and a scenario parent must have selected response bodies; its values keep the capture redaction markers, and a write whose URL it redacted ends the state of every type. For a retained record use source: {kind: "retained-evidence", evidence_id: "<parent_evidence_id>"}. Use normalized_result.parent_evidence_id from a summary receipt. Source kind is required. Portable source uses {kind: "inline", evidence: <complete Evidence>}. A completed Evidence JSON file on this host uses {kind: "evidence-file", path: "<absolute path>"}: REA reads and authenticates it, then projects it like retained Evidence, so a large capture is bounded by the file reader rather than by inline-input accounting. For layout, JavaScript, inventory, WASM, Mach-O, Interface Builder, EVM, managed artifact, Android package and Dart AOT sources, request a summary with view: {kind: "summary"}; summary is a view kind and has no facet. The WASM summary gives validation, counts, WAT digest/length, toolLine truncated
|
||||
inputSchema: inspectAnalysisViewInputSchema,
|
||||
outputSchema: evidenceResultOf(analysisViewResultSchema),
|
||||
examples: [
|
||||
|
||||
@@ -4,7 +4,8 @@ import {
|
||||
} from "../../domain/go/goBinary.js";
|
||||
import type { ToolContract } from "../toolContractTypes.js";
|
||||
import { toolContractMetadata } from "../toolEffects.js";
|
||||
import { evidenceResultOf } from "../toolOutputSchemaPrimitives.js";
|
||||
import { analysisDetailOutputSchemaOf } from "../analysisDetail.js";
|
||||
import { goBinarySummaryViewResultSchema } from "../../domain/analysisView/analysisView.js";
|
||||
|
||||
/** Static Go metadata is independent of active native-analysis providers and host toolchains. */
|
||||
export const GO_TOOL_CONTRACTS = [
|
||||
@@ -13,9 +14,12 @@ export const GO_TOOL_CONTRACTS = [
|
||||
...toolContractMetadata("inspect_go_binary"),
|
||||
kind: "artifact-provider",
|
||||
description:
|
||||
"Inspect embedded Go compiler version, main module, dependency/replacement versions and build settings in an explicit local ELF, PE or thin Mach-O image. Returns original artifact SHA-256, exact build-info source ranges, raw framed module bytes and unparsed module-text lines inline as observed Evidence. Supports inline and historical pointer encodings without installing Go, opening a disassembler, executing the target, writing temporary files or accessing the network. Missing metadata is reported as unknown, not proof that an image is not Go. Does not recover functions, source mappings, types or runtime behavior. Resource guards bound files to 256 MiB, aggregate embedded strings to 1 MiB, structural table decoding to 16 MiB and complete inline metadata to 8 MiB; malformed, unsupported and oversized inputs receive typed failures rather than partial success.",
|
||||
"Inspect embedded Go compiler version, main module, dependency/replacement versions and build settings in an explicit local ELF, PE or thin Mach-O image. Default detail complete returns original artifact SHA-256, exact build-info source ranges, raw framed module bytes and unparsed module-text lines inline as observed Evidence. Explicit detail summary retains the same complete parent and returns compiler/main-module identity, counts, source ranges and parent_evidence_id. Query retained rows through inspect_analysis_view kind go, collection dependencies or settings; select exact dependency paths or setting keys, indices, fields and pagination. Rows always retain their original index scoped to the parent Evidence; empty filters select none. Runtime dependency completeness remains unknown. Supports inline and historical pointer encodings without installing Go, opening a disassembler, executing the target, writing temporary files or accessing the network. Missing metadata is reported as unknown, not proof that an image is not Go. Does not recover functions, source mappings, types or runtime behavior. Resource guards bound files to 256 MiB, aggregate embedded strings to 1 MiB, structural table decoding to 16 MiB and complete inline metadata to 8 MiB; malformed, unsupported and oversized inputs receive typed failures rather than partial success.",
|
||||
inputSchema: inspectGoBinaryInputSchema,
|
||||
outputSchema: evidenceResultOf(goBinarySchema),
|
||||
outputSchema: analysisDetailOutputSchemaOf(
|
||||
goBinarySchema,
|
||||
goBinarySummaryViewResultSchema,
|
||||
),
|
||||
examples: [
|
||||
{
|
||||
title: "Inspect embedded build provenance",
|
||||
|
||||
@@ -92,7 +92,7 @@ export const ELECTRON_TOOL_CONTRACTS = [
|
||||
name: "analyze_javascript_application",
|
||||
...toolContractMetadata("analyze_javascript_application"),
|
||||
description:
|
||||
"Reconstruct one local ASAR or extracted JavaScript application as an inline application graph without executing it. Returns recovered graph nodes, edges, semantic relations, integrity contradictions, limitations, and coverage. Select source_paths to acquire and analyze only exact inventoried file paths; the complete artifact inventory and identity remain intact, and unselected sources remain explicit unknowns. Dependencies are not implicitly analyzed. Select detail summary to retain the complete analysis Evidence in this session and receive only its summary view, then inspect selected modules or pages with inspect_analysis_view. Summary detail reduces delivery, whereas source_paths bounds source analysis. Integrity mismatches fail by default; record-and-continue retains observed bytes as untrusted and marks graph coverage partial.",
|
||||
"Reconstruct one local ASAR or extracted JavaScript application as an inline application graph without executing it. Returns recovered graph nodes, edges, semantic relations, integrity contradictions, limitations, and coverage. Select source_paths to acquire and analyze only exact inventoried file paths; the complete artifact inventory and identity remain intact, and unselected sources remain explicit unknowns. Dependencies are not implicitly analyzed. Select detail summary to retain the complete analysis Evidence in this session and receive only its summary view, then inspect selected modules or pages with inspect_analysis_view. Summary detail reduces delivery, whereas source_paths bounds source analysis. Integrity mismatches fail by default; record-and-continue retains observed bytes as untrusted and marks graph coverage partial. Timeout recovery: returned provider_timeout uses the per-source analysis_timeout_ms budget. A host-only 'timed out awaiting tools/call' needs a longer client tool deadline, not a larger analysis_timeout_ms. Never repeat the unchanged call to test whether that timeout persists; report the host setting if configuration is not editable (Codex: mcp_servers.<server>.tool_timeout_sec, seconds).",
|
||||
kind: "electron-provider",
|
||||
inputSchema: analyzeJavaScriptApplicationRequestSchema,
|
||||
outputSchema: applicationOutputSchema,
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { inspectModelArtifactInputSchema } from "../../domain/model/modelArtifactQuery.js";
|
||||
import { z } from "zod";
|
||||
import { modelArtifactSchema } from "../../domain/model/modelArtifact.js";
|
||||
import {
|
||||
modelArtifactPageViewResultSchema,
|
||||
modelArtifactSummaryViewResultSchema,
|
||||
} from "../../domain/analysisView/analysisView.js";
|
||||
import type { ToolContract } from "../toolContractTypes.js";
|
||||
import { toolContractMetadata } from "../toolEffects.js";
|
||||
import { evidenceResultOf } from "../toolOutputSchemaPrimitives.js";
|
||||
|
||||
export const MODEL_TOOL_CONTRACTS = [
|
||||
{
|
||||
name: "inspect_model_artifact",
|
||||
...toolContractMetadata("inspect_model_artifact"),
|
||||
kind: "artifact-provider",
|
||||
description:
|
||||
"Inspect local GGUF v2/v3 or safetensors declarations by content, without loading models or executing code. Default hash header reads only the header: full-file SHA-256 and Evidence subject are unknown; artifact.header_sha256 is a scoped prefix commitment, not weight identity. hash full additionally streams all file bytes for SHA-256. Optional view returns a selected metadata/tensor/type page plus model context in this call. Summary retains complete Evidence for inspect_analysis_view kind model-artifact, collection metadata (keys/indices/fields) or tensors (names/types/indices/fields), with stable header-scoped indices and paging. Metadata arrays retain eight-element typed previews; scalar metadata is complete. Tensor counts, shapes, types and offsets are observed; parameter/storage totals are derived from declarations. Unknown GGUF types retain IDs with unknown byte lengths. Tensor payload, model behavior and inference quality are not inspected. One active inspection, 100 MB header ceiling and 32 MiB retained-facts budget; resource refusals are explicit. No network, external files, Python, model runtime, extraction or deserialization of pickle.",
|
||||
inputSchema: inspectModelArtifactInputSchema,
|
||||
outputSchema: evidenceResultOf(
|
||||
z.union([
|
||||
modelArtifactSchema,
|
||||
modelArtifactSummaryViewResultSchema,
|
||||
modelArtifactPageViewResultSchema,
|
||||
]),
|
||||
),
|
||||
examples: [
|
||||
{
|
||||
title: "Read model declarations without reading weights",
|
||||
input: {
|
||||
path: "/artifacts/model.gguf",
|
||||
hash: "header",
|
||||
detail: "summary",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
] as const satisfies readonly ToolContract[];
|
||||
@@ -4,6 +4,7 @@ import { WASM_TOOL_CONTRACTS } from "./wasm/wasmToolContracts.js";
|
||||
import { EVM_TOOL_CONTRACTS } from "./evm/evmToolContracts.js";
|
||||
import { DESKTOP_APPLICATION_TOOL_CONTRACTS } from "./desktopApplicationToolContracts.js";
|
||||
import { GO_TOOL_CONTRACTS } from "./go/goToolContracts.js";
|
||||
import { MODEL_TOOL_CONTRACTS } from "./model/modelToolContracts.js";
|
||||
import { IL2CPP_TOOL_CONTRACTS } from "./il2cpp/il2cppToolContracts.js";
|
||||
import { RUST_TOOL_CONTRACTS } from "./rust/rustToolContracts.js";
|
||||
import { SQLITE_TOOL_CONTRACTS } from "./sqlite/sqliteToolContracts.js";
|
||||
@@ -49,6 +50,7 @@ export const TOOL_CONTRACTS = [
|
||||
...EVM_TOOL_CONTRACTS,
|
||||
...GO_TOOL_CONTRACTS,
|
||||
...IL2CPP_TOOL_CONTRACTS,
|
||||
...MODEL_TOOL_CONTRACTS,
|
||||
...RUST_TOOL_CONTRACTS,
|
||||
...WASM_TOOL_CONTRACTS,
|
||||
...SQLITE_TOOL_CONTRACTS,
|
||||
|
||||
Loaded 100 of 224 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user