Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54e89bd5c8 |
@@ -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: "42"
|
||||
version: "41"
|
||||
---
|
||||
|
||||
# REA
|
||||
@@ -104,10 +104,6 @@ explicit path or endpoint and do not need it.
|
||||
consult the NativeAOT workflow in `references/native-and-artifacts.md` for
|
||||
optional metadata recovery and its supported host/layout boundary.
|
||||
- User-owned browser page already open: `list_browser_targets`.
|
||||
- Browser action the agent must perform (click, type, navigate):
|
||||
`capture_browser_scenario` with declared actions. See
|
||||
[runtime observation](references/runtime-observation.md) for connect/launch
|
||||
and the focused follow-up browser tools.
|
||||
- Fiddler Everywhere traffic or a native `.saz` capture: read the
|
||||
[Fiddler workflow](references/runtime-observation.md#fiddler-everywhere-traffic-debugging).
|
||||
Inspect saved SAZ with `inspect_web_network_capture` and `format: "saz"`;
|
||||
@@ -327,8 +323,7 @@ as complete while required questions remain open.
|
||||
[references/javascript-applications.md](references/javascript-applications.md)
|
||||
- Android APK declarations, classes, methods, and static references:
|
||||
[references/android-applications.md](references/android-applications.md)
|
||||
- Browser/Electron observation (passive and scenario) and static/runtime
|
||||
reconciliation:
|
||||
- Passive browser/Electron observation and static/runtime reconciliation:
|
||||
[references/runtime-observation.md](references/runtime-observation.md)
|
||||
- Evidence paging, comparisons, residual unknowns, and verification:
|
||||
[references/evidence-workflows.md](references/evidence-workflows.md)
|
||||
|
||||
@@ -169,6 +169,14 @@ repeat parsing. Returned-row exhaustion
|
||||
does not prove complete metadata or absence under partial/unavailable coverage;
|
||||
preserve build-local tokens and their artifact/MVID scope.
|
||||
|
||||
`inspect_managed_members` always returns the assembly-wide `member_refs`, even
|
||||
for one selected type. For questions about a selected type, select
|
||||
`detail: "summary"` with `type_names`; it returns the selection (with unmatched
|
||||
names), coverage and per-collection counts. Page rows with
|
||||
`view: {kind: "managed-members", collection: "call_edges", offset: 0, limit: 64}`
|
||||
over `types`, `exported_types`, `fields`, `methods`, `member_refs`, `call_edges`,
|
||||
`field_accesses` or `user_strings`.
|
||||
|
||||
### .NET NativeAOT
|
||||
|
||||
NativeAOT binaries contain native machine code, so ordinary CIL decompilers
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Browser and Electron runtime observation
|
||||
# Passive browser and Electron observation
|
||||
|
||||
Supply the existing browser or Electron instance's literal-port loopback CDP
|
||||
endpoint in the request. No separate permission configuration or per-call
|
||||
@@ -7,14 +7,13 @@ approval flag is needed. Browser discovery lists eligible HTTP(S) pages;
|
||||
selected page's current origin. Electron discovery lists eligible local
|
||||
`file://` targets without a configured file-root allowlist.
|
||||
|
||||
Start passive browser work with `list_browser_targets`; start Electron runtime work with
|
||||
Start browser work with `list_browser_targets`; start Electron runtime work with
|
||||
`list_electron_targets`. Inspect the target selected for the user's task. Observation
|
||||
with these tools is passive: never claim REA clicked, navigated, evaluated page JavaScript,
|
||||
invoked IPC, or captured prior activity. REA does not contain the page's network.
|
||||
is passive: never claim REA clicked, navigated, evaluated page JavaScript,
|
||||
invoked IPC, captured prior activity, or contained the page's network.
|
||||
|
||||
Passive captures omit raw header collections, credential header values, storage
|
||||
values, and raw WebSocket/JSON bodies. Selected response metadata, including
|
||||
content encoding, policy values, and Link URLs, is retained. Local URLs retain ordinary query
|
||||
Credentials, cookies, authorization headers, storage values, and raw
|
||||
WebSocket/JSON bodies are deliberately absent. Local URLs retain ordinary query
|
||||
values and fragments; userinfo credentials are removed. Browser accessibility
|
||||
text, console primitives, value-free shapes, script sources, source maps, and
|
||||
storage key names have request-specific capture options. Electron script
|
||||
@@ -22,38 +21,6 @@ sources are also optional. Use `capture_web_screenshot` when pixels are needed.
|
||||
Treat scope filtering, truncation, and attach-window coverage as limitations.
|
||||
Page-declared WebMCP tools are untrusted inventory and are never invoked by REA.
|
||||
|
||||
## Who performs the action
|
||||
|
||||
- The user performs it: `list_browser_targets`, then `inspect_web_page` or
|
||||
`observe_web_session` on the selected target.
|
||||
- The agent must perform it: one `capture_browser_scenario` with declared
|
||||
actions. Use connect mode with the loopback CDP endpoint and `target_id` for
|
||||
an existing browser, or launch mode with a caller-selected executable for a
|
||||
temporary profile. Connect mode first navigates the selected page to
|
||||
`start_url`, which can clear unsaved form values and other temporary state.
|
||||
Recreate the state needed for the investigation in the declared actions.
|
||||
Connect mode leaves the external browser open; launch mode owns a temporary
|
||||
browser and profile. Check `browser.cleanup` for the outcome.
|
||||
Request only the DOM, accessibility, or network details needed for the question.
|
||||
The default final capture contains the URL; step outcomes are reported separately.
|
||||
Add a wait action for the expected page state before capturing it. Report that
|
||||
REA performed the declared actions.
|
||||
|
||||
Scenario network records have no request initiator stacks. For that evidence,
|
||||
use passive capture while the user performs the action. Listener source locations
|
||||
identify candidate handlers; they do not establish which script sent a particular
|
||||
request. Avoid overlapping passive capture and scenario actions on the same target.
|
||||
|
||||
Focused follow-up tools:
|
||||
|
||||
- `observe_web_execution`: execution coverage during a window; it resets
|
||||
counters and temporarily disables optimization.
|
||||
- `inspect_web_event_listeners`: source locations for listeners registered
|
||||
directly on the first CSS match, without dispatching an event.
|
||||
- `compare_web_captures`: two normalized passive captures or recorded scenarios.
|
||||
- `discover_webmcp_tools`: inspect page-declared WebMCP inventory; never invoke
|
||||
the discovered tools.
|
||||
|
||||
Use `reconcile_javascript_runtime` only after static application Evidence and
|
||||
passive browser/Electron Evidence exist. Prefer captured-source digest identity;
|
||||
report path/digest disagreement as a mismatch and preserve ambiguity. Explicit
|
||||
|
||||
@@ -294,11 +294,7 @@ the first native server into an empty table, it copies the fallback's
|
||||
`mcpServers` entries into the native table so they keep loading, and leaves
|
||||
the fallback file untouched. Existing REA environment overrides are preserved
|
||||
when refreshing its command. A malformed or unreadable fallback file fails setup instead of
|
||||
being silently suppressed; repair or remove it and rerun. Uninstall removes an
|
||||
REA-owned `rea` entry from both the native table and the fallback, backing up
|
||||
each changed file, so an emptied native table cannot load a leftover fallback
|
||||
registration; other fallback servers, settings and a foreign `rea` entry are
|
||||
kept. ZCode disables a
|
||||
being silently suppressed; repair or remove it and rerun. ZCode disables a
|
||||
server with `enable: false` on its entry; doctor reports such a registration
|
||||
as stale, and rerunning setup removes the flag to repair it. Native configuration
|
||||
uses strict JSON; setup refuses comments, trailing commas and a BOM rather than
|
||||
|
||||
@@ -129,150 +129,6 @@ Setup pins package-runner MCP registrations to the exact installed REA version,
|
||||
installs the matching skill and on-demand references in the same plan, and adds
|
||||
`startup_timeout_sec = 30` for Codex and Grok Build.
|
||||
|
||||
## Limit REA to one project or agent profile
|
||||
|
||||
Setup registers REA for the whole client. For Claude Code it writes
|
||||
`mcpServers.rea` in `~/.claude.json` and installs the bundled skill under
|
||||
`~/.claude/skills`, so every project that Claude Code opens can call REA. To
|
||||
offer REA's tools only where they're wanted, use one of the routes below.
|
||||
|
||||
Both routes control which sessions discover REA's MCP server and skill. They
|
||||
leave shell commands, filesystem access and network access available. An agent
|
||||
under the same OS account can run `rea` from `PATH` or `npx` and read other
|
||||
profiles and REA output. For clean-room separation, use an implementation
|
||||
environment with filesystem and network restrictions that prevent reading or
|
||||
fetching REA, the target and analysis output, including through shared
|
||||
installations and package registries. A separate account or container must
|
||||
enforce those restrictions.
|
||||
|
||||
For Claude Code, check existing default-profile state before choosing either
|
||||
route. Setup is additive: a previous user-level REA registration and personal
|
||||
skill remain active in other projects. Back up the default configuration and
|
||||
REA skill first, then, with `CLAUDE_CONFIG_DIR` unset, remove the REA user
|
||||
registration with `claude mcp remove --scope user rea` and remove the known
|
||||
REA-installed `~/.claude/skills/reverse-engineer-anything` directory. Preserve
|
||||
unrelated registrations and skills. `rea uninstall` applies to other clients
|
||||
as well, so it is broader than this Claude-only migration.
|
||||
|
||||
**A separate Claude Code profile.** Claude Code keeps settings, skills and session
|
||||
history under `CLAUDE_CONFIG_DIR`. Setup follows the same
|
||||
variable: it writes the registration to `$CLAUDE_CONFIG_DIR/.claude.json` and
|
||||
the skill to `$CLAUDE_CONFIG_DIR/skills`.
|
||||
|
||||
```bash
|
||||
CLAUDE_CONFIG_DIR=~/.claude-rea npx rea-agents setup --client claude_code
|
||||
CLAUDE_CONFIG_DIR=~/.claude-rea claude
|
||||
```
|
||||
|
||||
A new profile may need authentication. Console sign-ins without an API key are
|
||||
stored outside `CLAUDE_CONFIG_DIR`; see
|
||||
[Claude Code authentication](https://code.claude.com/docs/en/authentication#log-in-with-multiple-accounts).
|
||||
Sessions started without the variable use the default profile's registration,
|
||||
skill and history.
|
||||
|
||||
Inline assignments apply only to that command. Keep the same prefix for every
|
||||
profile check, setup rerun, update, or removal; bare commands use the current
|
||||
environment and can otherwise inspect or recreate default-profile state:
|
||||
|
||||
```bash
|
||||
CLAUDE_CONFIG_DIR=~/.claude-rea claude mcp list
|
||||
CLAUDE_CONFIG_DIR=~/.claude-rea npx rea-agents doctor --client claude_code --skill --json
|
||||
CLAUDE_CONFIG_DIR=~/.claude-rea npx -y rea-agents@latest setup --client claude_code --dry-run
|
||||
```
|
||||
|
||||
After reviewing an update plan, rerun that same prefixed setup command without
|
||||
`--dry-run` and approve its exact changes. Profile removal likewise needs the
|
||||
prefix on `claude mcp remove --scope user rea` and any `rea uninstall` command;
|
||||
keep the broader client scope of uninstall in mind. Restart the selected
|
||||
profile after changing its pinned registration.
|
||||
|
||||
`claude mcp remove` removes only the registration. To remove REA from this
|
||||
profile without running the broader uninstall flow, first back up the profile's
|
||||
REA-managed skill directory, then remove
|
||||
`$CLAUDE_CONFIG_DIR/skills/reverse-engineer-anything` as well. Keep other skills
|
||||
in the profile intact.
|
||||
|
||||
The commands above use POSIX shell assignment syntax. In native Windows
|
||||
PowerShell, select the profile for the whole setup/launch/check session and
|
||||
restore the previous selection afterward:
|
||||
|
||||
```powershell
|
||||
$previousClaudeConfigDir = $env:CLAUDE_CONFIG_DIR
|
||||
try {
|
||||
$env:CLAUDE_CONFIG_DIR = Join-Path $HOME ".claude-rea"
|
||||
npx rea-agents setup --client claude_code
|
||||
claude mcp list
|
||||
npx rea-agents doctor --client claude_code --skill --json
|
||||
npx -y rea-agents@latest setup --client claude_code --dry-run
|
||||
# After reviewing the plan, repeat setup without --dry-run and approve it.
|
||||
claude
|
||||
# Profile removal, when intended, also belongs inside this selected scope:
|
||||
# claude mcp remove --scope user rea
|
||||
} finally {
|
||||
if ($null -eq $previousClaudeConfigDir) {
|
||||
Remove-Item Env:CLAUDE_CONFIG_DIR -ErrorAction SilentlyContinue
|
||||
} else {
|
||||
$env:CLAUDE_CONFIG_DIR = $previousClaudeConfigDir
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Use PowerShell for these Windows examples rather than pasting POSIX assignments
|
||||
into cmd.exe. Keep profile updates, readiness checks and removal inside the
|
||||
same selected environment; the `finally` block restores the previous profile
|
||||
or unsets the variable when it was absent.
|
||||
|
||||
**One project.** Install without configuring Claude Code: use the curl
|
||||
installer's `--no-setup`, or run setup only for other clients. Then register
|
||||
REA from the project directory at Claude Code's local scope.
|
||||
|
||||
This route uses the default Claude profile. Keep `CLAUDE_CONFIG_DIR` unset for
|
||||
the entire local lifecycle: registration, backup, listing, version replacement
|
||||
and removal. In POSIX shells run `unset CLAUDE_CONFIG_DIR`; in PowerShell run
|
||||
`Remove-Item Env:CLAUDE_CONFIG_DIR -ErrorAction SilentlyContinue` before these
|
||||
commands. Record an existing selection first if you intend to restore it after
|
||||
finishing. The configuration path below is the default profile's
|
||||
`~/.claude.json`; a different active profile uses its own config root instead.
|
||||
|
||||
Register with the variable unset:
|
||||
|
||||
<!-- x-release-please-start-version -->
|
||||
|
||||
```bash
|
||||
claude mcp add --scope local rea -- npx -y rea-agents@6.4.0 mcp
|
||||
```
|
||||
|
||||
On native Windows, use the same project directory and wrap the package-runner
|
||||
shim so the stdio client can launch it:
|
||||
|
||||
```powershell
|
||||
claude mcp add --scope local rea -- cmd.exe /d /c npx -y rea-agents@6.4.0 mcp
|
||||
```
|
||||
|
||||
<!-- x-release-please-end -->
|
||||
|
||||
Local scope stores the entry under that directory's path in `~/.claude.json`,
|
||||
so other projects don't load it. This route doesn't install the bundled skill.
|
||||
Avoid `--scope project`, which writes `.mcp.json` into the repository where it
|
||||
can be committed or copied. `rea doctor` and `rea uninstall` read only the
|
||||
user-level entry, so they don't report or remove a local-scope registration;
|
||||
remove it with `claude mcp remove --scope local rea` from the same directory.
|
||||
`rea update` doesn't refresh it either, so it stays on the version it names.
|
||||
To move it to a newer release, first back up the project entry in
|
||||
`~/.claude.json` and record its environment and other metadata. In particular,
|
||||
preserve caller-selected overrides such as `REA_MCP_INPUT_SCHEMA_PROFILE` and
|
||||
`REA_MCP_MAX_RESPONSE_BYTES`. Run `claude mcp remove --scope local rea` from the
|
||||
same directory, then recreate it with the platform-appropriate command above
|
||||
and the new version. Reapply every recorded environment value with Claude
|
||||
Code's `mcp add --env NAME=value` options and restore any other retained entry
|
||||
metadata before restarting the project session. Removal and re-addition do not
|
||||
preserve these values automatically. Running setup for Claude Code instead
|
||||
would add a user-level entry for every project.
|
||||
|
||||
Check the separate profile with the prefixed `claude mcp list` above and compare
|
||||
with the default profile with `CLAUDE_CONFIG_DIR` unset. For local scope, run
|
||||
`claude mcp list` from the selected project and again from another project.
|
||||
|
||||
## Check readiness for your task
|
||||
|
||||
Run `rea doctor` when you need diagnosis. It reads host prerequisites and agent
|
||||
|
||||
@@ -81,6 +81,40 @@ including the existing `inspect-analysis-view` CLI. Session record lifetime and
|
||||
export/import semantics remain unchanged. A server without Evidence retention
|
||||
rejects selected summary before analysis; complete delivery remains available.
|
||||
|
||||
### Member summary and retained member rows
|
||||
|
||||
Complete `inspect_managed_members` delivery always includes the assembly-wide
|
||||
`member_refs` and `exported_types`, which `type_names` does not narrow, so even
|
||||
one selected or unmatched type can return megabytes. Select
|
||||
`{"path":"/absolute/path/Assembly.dll","type_names":["Namespace.Type"],"detail":"summary"}`
|
||||
to keep the complete source in the session and receive identity, metadata
|
||||
status, the selection with its unmatched names, coverage/issues, the separate
|
||||
user-string coverage, limitations and per-collection counts. Then page original
|
||||
rows with view kind `managed-members`:
|
||||
|
||||
```json
|
||||
{
|
||||
"source": {
|
||||
"kind": "retained-evidence",
|
||||
"evidence_id": "<parent_evidence_id>"
|
||||
},
|
||||
"view": {
|
||||
"kind": "managed-members",
|
||||
"collection": "call_edges",
|
||||
"offset": 0,
|
||||
"limit": 64
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Collections are `types`, `exported_types`, `fields`, `methods`, `member_refs`,
|
||||
`call_edges`, `field_accesses` and `user_strings` (the literals). Each page names
|
||||
its collection, retains original rows, order, tokens, signatures and body facts,
|
||||
and carries the source selection and coverage; `user_strings` pages carry the
|
||||
literal pool's own coverage. Older Evidence that never examined exported types
|
||||
or user strings reports a null count and refuses those pages rather than
|
||||
returning an empty one.
|
||||
|
||||
## Shipped scope
|
||||
|
||||
The canonical parser admits PE/CLI bytes and their metadata/CIL without loading
|
||||
|
||||
+25
-22
@@ -20,7 +20,7 @@
|
||||
|
||||
**[웹사이트](https://rea.tools/ko/) · [가이드](https://rea.tools/ko/guides/) · [사례](https://rea.tools/ko/showcase/)**
|
||||
|
||||
[빠른 시작](#빠른-시작) · [REA의 작동 방식](#rea의-작동-방식) · [분석할 수 있는 대상](#분석할-수-있는-대상) · [사례](#사례) · [자주 묻는 질문](#자주-묻는-질문) · [문서](#문서)
|
||||
[빠른 시작](#빠른-시작) · [REA의 작동 방식](#rea의-작동-방식) · [분석할 수 있는 대상](#분석할-수-있는-대상) · [사례](#사례) · [자주 묻는 질문](#자주-묻는-질문) · [GNU objdump 및 Rizin 도구](../reverse-engineering-tools.md) · [문서](#문서)
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
|
||||
REA는 에이전트를 분석 도구에 연결해 네이티브 바이너리, JavaScript와 Electron 앱, .NET 어셈블리, 웹사이트를 검사할 수 있게 합니다. 터미널에서도 같은 도구를 사용할 수 있습니다. 분석은 로컬에서 실행되며, 결과에는 각 결론의 근거와 한계가 포함됩니다.
|
||||
|
||||
설정 과정은 에이전트에 REA를 등록하고 해당 버전에 맞는 워크플로 지침을 설치합니다. 네이티브 분석에는 기존 Hopper, Ghidra 또는 IDA 설치를 사용할 수 있습니다. 승인하면 설정 과정에서 Hopper를 설치할 수도 있습니다. 정적 JavaScript 분석에는 네이티브 분석 엔진이 필요하지 않습니다.
|
||||
설정 과정은 에이전트에 REA를 등록하고 해당 버전에 맞는 워크플로 지침을 설치합니다. 네이티브 분석에는 기존 Hopper 또는 Ghidra 설치를 사용할 수 있습니다. 승인하면 설정 과정에서 Hopper를 설치할 수도 있습니다. 정적 JavaScript 분석에는 두 엔진 모두 필요하지 않습니다.
|
||||
|
||||
> **[REA 웹사이트](https://rea.tools/ko/)**에서 설정 방법, 그림으로 설명한 가이드, 실제 사례를 확인하세요.
|
||||
|
||||
@@ -105,14 +105,12 @@ rea --help
|
||||
|
||||
REA는 빠르게 바뀌며 새 릴리스에는 버그 수정이 자주 포함됩니다. 설치된 버전을 최신으로 유지하세요.
|
||||
|
||||
`npm install --global` 또는 `bun add --global`로 설치한 CLI에서는 다음 명령을 사용합니다.
|
||||
npm으로 설치한 CLI에서는 다음 명령을 사용합니다.
|
||||
|
||||
```bash
|
||||
rea update
|
||||
```
|
||||
|
||||
Bun 전역 업데이트에는 Bun 1.2.17 이상이 필요합니다. 이전 버전의 Bun을 사용한다면 `rea update`를 실행하기 전에 Bun을 직접 업그레이드하세요.
|
||||
|
||||
에이전트 등록과 스킬을 갱신하려면 업데이트 과정에서 출력된 설정 명령을 실행하세요.
|
||||
|
||||
`npx`를 사용한다면 다음 명령으로 에이전트 설정을 갱신하세요.
|
||||
@@ -121,7 +119,7 @@ Bun 전역 업데이트에는 Bun 1.2.17 이상이 필요합니다. 이전 버
|
||||
npx rea-agents@latest setup
|
||||
```
|
||||
|
||||
설정 변경 사항을 검토하고 에이전트를 다시 시작하세요. 일회성 CLI 작업에는 `npx rea-agents@latest` 뒤에 필요한 명령을 붙여 실행하세요. Bun을 사용한다면 위 명령의 `npx rea-agents@latest`를 `bunx rea-agents@latest`로 바꿔 실행하세요.
|
||||
설정 변경 사항을 검토하고 에이전트를 다시 시작하세요. 일회성 CLI 작업에는 `npx rea-agents@latest` 뒤에 필요한 명령을 붙여 실행하세요.
|
||||
|
||||
## REA의 작동 방식
|
||||
|
||||
@@ -141,16 +139,14 @@ REA에는 Node.js 22.x(>=22.19), 24.x(>=24.11) 또는 26+와 npm이 필요합니
|
||||
| --------------------- | ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 네이티브 바이너리 | 의사 코드, 어셈블리, 문자열, 심볼, 호출, 참조 | Hopper, Ghidra 또는 IDA; [네이티브 분석](https://rea.tools/ko/guides/native/) |
|
||||
| 오프라인 ELF 레이아웃 | 섹션, 세그먼트, 원본 심볼/재배치 정보, 정적 분석으로 얻은 보안 완화 기법 후보 | Linux x64에서 호출자가 제공하는 pwntools; [바이너리 진단](../binary-diagnostics.md) |
|
||||
| Go 바이너리 | 내장된 컴파일러 버전, 모듈, `replace` 지시어, 빌드 설정 | REA 내장 ELF/PE/thin Mach-O 정적 검사; Go 설치 불필요 |
|
||||
| Rust 바이너리 | cargo-auditable이 내장한 크레이트 이름, 버전, 소스, 의존성 관계 | REA 내장 ELF/PE/thin Mach-O 정적 검사; [Rust 바이너리](../rust-binaries.md) |
|
||||
| WASM 아티팩트 | 유효성 검사, 섹션, import/export의 정확한 WAT 형식, 디코딩된 WAT | 호출자 제공 WABT 1.0.42; `inspect_wasm_artifact` |
|
||||
| EVM 바이트코드 | 디스패치 선택자, 바이트 오프셋, 추론한 인자와 상태 변경 가능 여부 | Linux x64의 로컬 원시 바이트/16진수 입력 파일; [오프라인 EVM 가이드](../evm-bytecode.md) |
|
||||
| SQLite 데이터베이스 | 테이블, 열, 인덱스, 뷰, 트리거, 선택한 일반 테이블의 행 | 로컬 데이터베이스/WAL 스냅샷; Node.js 24.15 이상의 24.x 또는 26 이상(네이티브 SQLite 한도 적용) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| 기록된 Linux 크래시 | 원시 note 레코드, 기록된 모든 스레드의 레지스터/시그널, 선택적 매핑 후보 | Linux x64에서 호출자가 제공하는 pwntools; GDB/pwndbg는 선택 사항; [기록된 크래시](../recorded-crashes.md) |
|
||||
| JavaScript / Electron | 모듈, 임포트, 소스 맵, 라우트, IPC, 네이티브 애드온 관계 | Node.js와 npm; [애플리케이션 분석](https://rea.tools/ko/guides/javascript/) |
|
||||
| 웹사이트 | 페이지 구조, 스크립트, 네트워크 관찰 결과, 요청한 스크린샷 | Chrome 계열 브라우저; [브라우저 분석](https://rea.tools/ko/guides/browser/) |
|
||||
| 저장된 네트워크 캡처 | 요청, 응답, 접근 가능한 페이로드, 소스 위치 | HAR 또는 Fiddler Everywhere SAZ; mitmproxy 네이티브 캡처에는 Linux의 mitmdump; [캡처 가이드](../web-network-captures.md) |
|
||||
| .NET 어셈블리 | 메타데이터, CIL 명령, 선언된 네이티브 의존성, 빌드 비교 | 별도 도구 불필요(정적 검사만); [관리 코드 가이드](../managed-code-analysis.md) |
|
||||
| 저장된 네트워크 캡처 | 요청, 응답, 접근 가능한 페이로드, 소스 위치 | HAR; mitmproxy 네이티브 캡처에는 Linux의 mitmdump; [캡처 가이드](../web-network-captures.md) |
|
||||
| 패킷 캡처 | 엔드포인트, DNS, TLS SNI, 프로토콜 분포, HTTP 요청 | 호출자 제공 tshark 및 capinfos; [pcap 가이드](../pcap-analysis.md) |
|
||||
| .NET 어셈블리 | 메타데이터, CIL 명령, 선언된 네이티브 의존성, 빌드 비교 | 정적 검사; [관리 코드 가이드](../managed-code-analysis.md) |
|
||||
| Android APK | 매니페스트 선언, 클래스, 디컴파일된 메서드, 참조 | Linux/macOS/Windows x64의 헤드리스 JADX와 전체 JDK; [Android 가이드](../android-analysis.md) |
|
||||
| Android 기기 | 기기, 패키지, 프로세스, 로그, 덤프, 화면 캡처, APK/파일 전송 | 호출자 제공 adb(에뮬레이터 또는 실제 기기); [ADB 가이드](../adb-device-analysis.md) |
|
||||
| JEB 프로젝트 | 엔진 기반 프로젝트 유닛, 아티팩트 해시, 타입/메서드 의사코드 | 호출자가 시작하고 MCP를 제공하는 JEB 클라이언트(REA_JEB_MCP_URL로 선택); [JEB 가이드](../jeb-analysis.md) |
|
||||
@@ -158,18 +154,15 @@ REA에는 Node.js 22.x(>=22.19), 24.x(>=24.11) 또는 26+와 npm이 필요합니
|
||||
| Flutter 앱 | Dart 스냅샷 해시, ABI별 라이브러리 해시, 엔진 빌드 출처 | 없음 (정적 APK 파싱); [Flutter 가이드](../flutter-analysis.md) |
|
||||
| 펌웨어 | 영역, 추출 결과, 네이티브 분석으로 전달할 정보 | Linux의 Binwalk / Unblob; [펌웨어 가이드](../firmware-analysis.md) |
|
||||
| 기록된 패킷 | 선택 필드, 반복 값, 필터 및 디코딩 규칙 | Linux: TShark 4.x + prlimit; [캡처 가이드](../packet-captures.md) |
|
||||
| 패킷 요약 | 엔드포인트, DNS, TLS SNI, 프로토콜 분포, HTTP 요청 | 호출자 제공 tshark 및 capinfos(Linux에서 검증됨); [pcap 가이드](../pcap-analysis.md) |
|
||||
| 기록된 논리 신호 | 디코더 주석, 샘플 범위, 진단 정보 | Linux: sigrok-cli 0.7.2 + prlimit; [신호 기록 가이드](../signal-recordings.md) |
|
||||
| 패키지 및 리소스 | 파일 목록, 다이제스트, plist, Apple 번들 구조, 추출한 리소스 | [아티팩트 및 JavaScript 가이드](../javascript-artifact-reconstruction.md), [Apple 애플리케이션](../apple-application-analysis.md) |
|
||||
| 프로세스 동작 | 터미널 출력, 상호작용, 종료 및 파일 시스템 관찰 결과, 실행 비교 | 네이티브 PTY를 지원하는 Linux/macOS; [프로세스 캡처](../process-capture.md) |
|
||||
|
||||
정적 JavaScript 및 .NET 검사는 제공된 파일을 읽으며 애플리케이션을 실행하지 않습니다. 런타임 캡처는 사용자의 권한으로 선택한 대상을 실행하거나 대상과 상호작용합니다. 각 런타임 가이드에서 그 영향을 설명합니다.
|
||||
|
||||
오프라인 네이티브 바이너리 검사에는 호출자가 제공하는 GNU objdump와 Rizin도 사용할 수 있습니다. 제공자 설정, CLI 명령, Evidence의 한계는 [리버스 엔지니어링 도구 가이드](../reverse-engineering-tools.md)를 참고하세요. REA는 objdump와 Rizin을 설치하거나 업데이트하지 않습니다.
|
||||
|
||||
<a id="choosing-a-deep-analysis-provider"></a>
|
||||
|
||||
지원하는 네이티브 형식과 호스트는 제공자마다 다릅니다. [Hopper 및 Ghidra 설정](../installation.md#hopper), [IDA 가이드](../ida-provider.md)(선택 사항인 네이티브 MCP 브리지 포함), [실험적 Windows Ghidra 지원](../windows-ghidra-p0.md)을 참고하세요. Ghidra는 [16비트 DOS 분석](../ghidra-dos.md)도 지원합니다. 큰 바이너리를 분석할 때는 `REA_GHIDRA_STARTUP_TIMEOUT_MS` 또는 `REA_HOPPER_STARTUP_TIMEOUT_MS`로 제공자의 시작 제한 시간을 늘리세요. 제공자 선택은 [CLI 가이드](../cli.md#choose-a-provider)를 참고하세요. 최신 npm 릴리스 이후 추가된 기능은 [릴리스에서의 사용 가능 여부](../installation.md#released-package-and-main)를 확인하세요.
|
||||
지원하는 네이티브 형식과 호스트는 제공자마다 다릅니다. [Hopper 및 Ghidra 설정](../installation.md#hopper), [IDA 가이드](../ida-provider.md), [실험적 Windows Ghidra 지원](../windows-ghidra-p0.md)을 참고하세요. Ghidra는 [16비트 DOS 분석](../ghidra-dos.md)도 지원합니다. 제공자 선택은 [CLI 가이드](../cli.md#choose-a-provider)를 참고하세요. 최신 npm 릴리스 이후 추가된 기능은 [릴리스에서의 사용 가능 여부](../installation.md#released-package-and-main)를 확인하세요.
|
||||
|
||||
## 사례
|
||||
|
||||
@@ -216,7 +209,7 @@ REA로 흥미로운 대상을 분석했다면 알려 주세요. [issue](https://
|
||||
<details>
|
||||
<summary><strong>Hopper를 먼저 실행해야 하나요?</strong></summary>
|
||||
|
||||
REA는 작업에 필요할 때 Hopper를 실행합니다. macOS에서는 처음 실행할 때 데모 모드를 선택하거나 라이선스를 활성화하라는 대화 상자가 나타날 수 있습니다. 첫 CLI/MCP 작업이나 무인 작업을 실행하기 전에 같은 사용자 세션에서 이 대화 상자를 완료하세요. 필요하면 Hopper를 직접 여세요. [Hopper 실행 및 문제 해결](../installation.md#launcher-paths-and-troubleshooting)을 참고하세요.
|
||||
REA는 작업에 필요할 때 Hopper를 실행합니다. macOS에서는 처음 실행할 때 데모 모드를 선택하거나 라이선스를 활성화하라는 대화 상자가 나타날 수 있습니다. [Hopper 실행 및 문제 해결](../installation.md#launcher-paths-and-troubleshooting)을 참고하세요.
|
||||
|
||||
</details>
|
||||
|
||||
@@ -246,7 +239,7 @@ REA는 대상을 로컬에서 분석합니다. 에이전트는 도구 결과를
|
||||
|
||||
먼저 업데이트하세요. 최근 릴리스에서 이미 수정되었을 수 있습니다.
|
||||
|
||||
`npm install --global` 또는 `bun add --global`로 설치한 CLI에서는 다음 명령을 사용합니다.
|
||||
npm으로 설치한 CLI에서는 다음 명령을 사용합니다.
|
||||
|
||||
```bash
|
||||
rea update
|
||||
@@ -293,10 +286,12 @@ REA를 사용하고, 버그를 보고하고, 기능을 요청하고, 빌드를
|
||||
|
||||
책임 있는 사용에 관한 안내는 [DISCLAIMER.md](../../DISCLAIMER.md)를 참고하세요.
|
||||
|
||||
불법적이거나 허가되지 않은 사용에 대한 프로젝트의 입장은 모든 REA 버전, 배포 채널, 플랫폼에 적용되며, 각 사본에는 해당 라이선스가 그대로 유지됩니다.
|
||||
|
||||
REA는 법을 준수하는 연구, 교육, 상호운용성 확보, 소프트웨어 보존, 그리고 본인이 소유하거나 조사 권한을 가진 소프트웨어의 분석을 위한 도구입니다.
|
||||
|
||||
The project's position on illegal or unauthorized use covers all REA
|
||||
versions, distribution channels, and platforms; each copy retains its
|
||||
applicable licenses.
|
||||
|
||||
## 기여
|
||||
|
||||
REA에 도움을 보태 주세요! [issue를 등록](https://github.com/morluto/rea/issues)해 버그나 기능을 제안하거나, [pull request를 보내](https://github.com/morluto/rea/pulls) 코드와 문서를 개선할 수 있습니다.
|
||||
@@ -307,6 +302,14 @@ REA에 도움을 보태 주세요! [issue를 등록](https://github.com/morluto/
|
||||
|
||||
[MIT](../../LICENSE)
|
||||
|
||||
REA가 작성한 코드는 MIT 라이선스를 따릅니다. 제3자 자료에는 각자의 라이선스와 저작권 고지가 그대로 유지됩니다. 의존성 목록, 보존된 라이선스 원문, 외부 도구 약관은 [제3자 고지](../../THIRD_PARTY_NOTICES.md)를 참고하세요.
|
||||
REA-authored code is licensed under MIT. Third-party material retains its own
|
||||
licenses and copyright notices; see [Third-party notices](../../THIRD_PARTY_NOTICES.md)
|
||||
for the dependency inventory, preserved license texts, and external-tool terms.
|
||||
|
||||
IDA Pro / Hex-Rays 디컴파일러, Hopper, JEB 등 상용 도구는 해당 라이선스에 따라 **합법적으로 취득하고 사용 권한을 갖춰야 합니다.** REA는 어떤 도구의 라이선스, 키, 활성화 권한도 부여하지 않습니다. 체험판과 데모는 사용자, 기능, 자동화, 출력 공유 제한을 포함한 공급업체 약관을 따릅니다. 크랙 버전, 도난 키, 무단 키 공유, 라이선스 제한 우회는 허용되지 않습니다. 이 안내는 MIT 라이선스나 제3자 라이선스를 변경하지 않습니다.
|
||||
**You must legally obtain and be authorized to use commercial tools**, including
|
||||
IDA Pro / Hex-Rays decompilers, Hopper, and JEB, under their applicable licenses.
|
||||
REA grants no tool license, key, or activation entitlement. Trials and demos
|
||||
remain subject to vendor terms, including user, feature, automation, and output
|
||||
sharing restrictions. Cracked copies, stolen keys, unauthorized key sharing,
|
||||
and bypassing license restrictions are not authorized. This guidance does not
|
||||
amend MIT or third-party licenses.
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import {
|
||||
access,
|
||||
copyFile,
|
||||
mkdir,
|
||||
mkdtemp,
|
||||
readFile,
|
||||
readdir,
|
||||
rmdir,
|
||||
rm,
|
||||
stat,
|
||||
symlink,
|
||||
@@ -55,7 +54,7 @@ try {
|
||||
join(workspace, "dist", "windows", "WindowsNativeLoader.js"),
|
||||
).href;
|
||||
const source = `import assert from 'node:assert/strict';
|
||||
import {existsSync,mkdirSync,readFileSync,readdirSync,renameSync,statSync,writeFileSync} from 'node:fs';
|
||||
import {existsSync,mkdirSync,readdirSync,renameSync,statSync} from 'node:fs';
|
||||
import {join} from 'node:path';
|
||||
import {systemWindowsNativeAuthority} from ${JSON.stringify(loader)};
|
||||
const temp=process.env.TEMP;
|
||||
@@ -65,61 +64,46 @@ assert.equal(first.retryable,true);
|
||||
assert.match(first.reason,/Delete owned object by handle failed.*Win32 5/s);
|
||||
const [name,...extra]=readdirSync(temp);assert.ok(name);assert.equal(extra.length,0);
|
||||
const path=join(temp,name);const identity=statSync(path,{bigint:true});
|
||||
// Delete sharing permits rename; ownership follows the retained NTFS handle.
|
||||
writeFileSync(join(path,'owned-child.txt'),'owned child');
|
||||
const renamed=path+'-renamed';renameSync(path,renamed);
|
||||
const moved=statSync(renamed,{bigint:true});
|
||||
assert.equal(moved.dev,identity.dev);assert.equal(moved.ino,identity.ino);
|
||||
// Ownership follows the retained NTFS handle even when the directory moves.
|
||||
// A replacement at its old pathname must remain caller-owned.
|
||||
const moved=path+'-renamed';renameSync(path,moved);
|
||||
assert.equal(statSync(moved,{bigint:true}).ino,identity.ino);
|
||||
mkdirSync(path);const replacement=statSync(path,{bigint:true});
|
||||
assert.notEqual(replacement.ino,identity.ino);
|
||||
const sentinel=join(path,'unrelated.txt');writeFileSync(sentinel,'unrelated replacement');
|
||||
if(process.argv[2]!=='teardown'){
|
||||
const second=systemWindowsNativeAuthority();assert.equal(second.available,true);
|
||||
assert.equal(existsSync(renamed),false);
|
||||
assert.equal(existsSync(moved),false);
|
||||
assert.deepEqual(readdirSync(temp),[name]);
|
||||
assert.equal(readFileSync(sentinel,'utf8'),'unrelated replacement');
|
||||
assert.equal(statSync(path,{bigint:true}).ino,replacement.ino);
|
||||
assert.equal(systemWindowsNativeAuthority(),second);
|
||||
}
|
||||
console.log(JSON.stringify({mode:process.argv[2],firstFailed:true,retainedOwner:true,name,renamed,device:identity.dev.toString(),inode:identity.ino.toString(),replacementInode:replacement.ino.toString()}));
|
||||
console.log(JSON.stringify({mode:process.argv[2],firstFailed:true,retainedOwner:true,inode:identity.ino.toString(),replacementName:name,replacementInode:replacement.ino.toString()}));
|
||||
`;
|
||||
const fixture = join(workspace, "probe.mjs");
|
||||
await writeFile(fixture, source);
|
||||
for (const mode of ["retry", "teardown"]) {
|
||||
const temp = join(workspace, mode);
|
||||
await mkdir(temp);
|
||||
const child = spawnSync(process.execPath, [fixture, mode], {
|
||||
encoding: "utf8",
|
||||
timeout: 20_000,
|
||||
windowsHide: true,
|
||||
env: { ...process.env, TEMP: temp, TMP: temp },
|
||||
});
|
||||
if (child.error !== undefined) throw child.error;
|
||||
assert.equal(child.status, 0, child.stderr);
|
||||
assert.equal(
|
||||
child.stderr,
|
||||
"",
|
||||
"Native environment teardown reported a cleanup failure",
|
||||
const observed = JSON.parse(
|
||||
execFileSync(process.execPath, [fixture, mode], {
|
||||
encoding: "utf8",
|
||||
timeout: 20_000,
|
||||
windowsHide: true,
|
||||
env: { ...process.env, TEMP: temp, TMP: temp },
|
||||
}),
|
||||
);
|
||||
const observed = JSON.parse(child.stdout);
|
||||
assert.equal(observed.mode, mode);
|
||||
assert.equal(observed.firstFailed, true);
|
||||
assert.equal(observed.retainedOwner, true);
|
||||
await assert.rejects(access(observed.renamed), { code: "ENOENT" });
|
||||
assert.deepEqual(await readdir(temp), [observed.name]);
|
||||
const replacement = join(temp, observed.name);
|
||||
const identity = await stat(replacement, { bigint: true });
|
||||
assert.equal(identity.dev.toString(), observed.device);
|
||||
assert.equal(identity.ino.toString(), observed.replacementInode);
|
||||
assert.notEqual(identity.ino.toString(), observed.inode);
|
||||
assert.deepEqual(await readdir(replacement), ["unrelated.txt"]);
|
||||
assert.deepEqual(await readdir(temp), [observed.replacementName]);
|
||||
const replacement = join(temp, observed.replacementName);
|
||||
assert.equal(
|
||||
await readFile(join(replacement, "unrelated.txt"), "utf8"),
|
||||
"unrelated replacement",
|
||||
(await stat(replacement, { bigint: true })).ino.toString(),
|
||||
observed.replacementInode,
|
||||
);
|
||||
await rmdir(replacement);
|
||||
assert.deepEqual(await readdir(temp), []);
|
||||
}
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ ok: true, controlledDispositionFailure: true, retainedOwner: true, renamedOwnerCleansChildren: true, unrelatedReplacementPreserved: true, retryRecovers: true, successfulAdmissionCached: true, environmentTeardownCleansProbe: true, issue1701HostCauseReproduced: false })}\n`,
|
||||
`${JSON.stringify({ ok: true, controlledDispositionFailure: true, retainedOwner: true, renamedOwnerCleanup: true, replacementPreserved: true, retryRecovers: true, successfulAdmissionCached: true, environmentTeardownCleansProbe: true, issue1701HostCauseReproduced: false })}\n`,
|
||||
);
|
||||
} finally {
|
||||
await rm(workspace, { recursive: true, force: true });
|
||||
|
||||
@@ -167,7 +167,7 @@ export const parseZcodeFallbackConfiguration = (
|
||||
? {}
|
||||
: requireObject(document.mcpServers, "ZCode fallback mcpServers"),
|
||||
serversPath: ["mcpServers"],
|
||||
dialect: "zcode_fallback",
|
||||
dialect: "json",
|
||||
legacyServers: {},
|
||||
};
|
||||
};
|
||||
@@ -183,7 +183,6 @@ export const parseClientConfiguration = (
|
||||
if (format === undefined || format === "unsupported")
|
||||
throw new TypeError("client does not have a supported MCP config format");
|
||||
// DeepSeek Harness stores a Cordis patch list, not a server table.
|
||||
if (format === "zcode_fallback") return parseZcodeFallbackConfiguration(text);
|
||||
if (format === "dsh") return parseDshCordisDocument(text);
|
||||
const document = parseDocument(text, format);
|
||||
const serversKey = clientConfigurationServersKey(format);
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.j
|
||||
import type { AnalysisCleanupObservation } from "../domain/analysisErrorBase.js";
|
||||
import { readReferenceSource } from "../reference/ReferenceSourceReader.js";
|
||||
import type {
|
||||
ReferenceSourceExclusionKind,
|
||||
ReferenceSourceEntryKind,
|
||||
ReferenceSourceRead,
|
||||
ReferenceSourceReaderError,
|
||||
} from "../reference/ReferenceSourceReaderTypes.js";
|
||||
@@ -111,8 +111,7 @@ const deriveInventoryState = (
|
||||
entry.limitations.length > 0 ||
|
||||
(entry.kind === "file" && entry.content_state !== "hashed") ||
|
||||
(entry.kind === "directory" && entry.tree_state !== "enumerated") ||
|
||||
(entry.kind === "symlink" && entry.target_state !== "internal") ||
|
||||
entry.kind === "unknown",
|
||||
(entry.kind === "symlink" && entry.target_state !== "internal"),
|
||||
);
|
||||
if (partialEntry) return "partial";
|
||||
const partialRelationship = input.relationships.some(
|
||||
@@ -143,24 +142,11 @@ const createShouldExclude =
|
||||
exclusions: HistoricalSourceGraphInput["exclusions"],
|
||||
secrets: PreparedReferenceSourceImport["secrets"],
|
||||
ignored: PreparedReferenceSourceImport["ignored"],
|
||||
): ((path: string, kind: ReferenceSourceExclusionKind) => boolean) =>
|
||||
): ((path: string, kind: ReferenceSourceEntryKind) => boolean) =>
|
||||
(path, kind) => {
|
||||
// An entry of unknown kind may be a directory, so directory-only rules
|
||||
// must still exclude it; this fails closed for caller-marked secrets.
|
||||
const patternPaths =
|
||||
kind === "directory"
|
||||
? [`${path}/`]
|
||||
: kind === "unknown"
|
||||
? [path, `${path}/`]
|
||||
: [path];
|
||||
const firstMatch = (matcher: typeof secrets) => {
|
||||
const results = patternPaths.map((patternPath) =>
|
||||
matcher.test(patternPath),
|
||||
);
|
||||
return results.find(({ ignored }) => ignored) ?? results[0];
|
||||
};
|
||||
const secretMatch = firstMatch(secrets);
|
||||
if (secretMatch?.ignored === true) {
|
||||
const patternPath = kind === "directory" ? `${path}/` : path;
|
||||
const secretMatch = secrets.test(patternPath);
|
||||
if (secretMatch.ignored) {
|
||||
if (!secretMatch.rule)
|
||||
throw new Error(`Ignored secret path has no matching rule: ${path}`);
|
||||
exclusions.push({
|
||||
@@ -170,8 +156,8 @@ const createShouldExclude =
|
||||
});
|
||||
return true;
|
||||
}
|
||||
const match = firstMatch(ignored);
|
||||
if (match?.ignored !== true) return false;
|
||||
const match = ignored.test(patternPath);
|
||||
if (!match.ignored) return false;
|
||||
if (!match.rule)
|
||||
throw new Error(`Ignored path has no matching rule: ${path}`);
|
||||
const reason = match.rule.mark;
|
||||
|
||||
@@ -45,8 +45,6 @@ const entryRecoveryGuidance = (
|
||||
return "This directory could not be read. Check its permissions, then try again.";
|
||||
if (entry.kind === "symlink")
|
||||
return "This symbolic link could not be read safely. Check the link and its permissions, then try again.";
|
||||
if (entry.kind === "unknown")
|
||||
return "This entry's type could not be determined. Check the parent directory and try again.";
|
||||
return "This file could not be read. Check its permissions, then try again.";
|
||||
};
|
||||
|
||||
@@ -75,13 +73,6 @@ const failedEntry = (
|
||||
classifications,
|
||||
limitations: [limitation],
|
||||
};
|
||||
if (entry.kind === "unknown")
|
||||
return {
|
||||
path: entry.path,
|
||||
kind: "unknown",
|
||||
classifications: ["unknown"],
|
||||
limitations: [limitation],
|
||||
};
|
||||
return {
|
||||
path: entry.path,
|
||||
kind: "file",
|
||||
|
||||
@@ -47,7 +47,6 @@ export interface SetupClient {
|
||||
| "pi"
|
||||
| "kimi"
|
||||
| "hermes"
|
||||
| "zcode_fallback"
|
||||
| "zcode"
|
||||
| "dsh"
|
||||
| "unsupported";
|
||||
|
||||
@@ -231,21 +231,12 @@ export const systemUninstallHost = (
|
||||
const clients = supportedClients(home, platform, environment);
|
||||
redirection = appDataRedirection(clients);
|
||||
return Promise.resolve(
|
||||
clients.flatMap((client) => [
|
||||
...uninstallConfigPaths(client).map((configPath) => ({
|
||||
clients.flatMap((client) =>
|
||||
uninstallConfigPaths(client).map((configPath) => ({
|
||||
...client,
|
||||
configPath,
|
||||
})),
|
||||
...(client.zcodeFallbackPath === undefined
|
||||
? []
|
||||
: [
|
||||
{
|
||||
...client,
|
||||
configPath: client.zcodeFallbackPath,
|
||||
format: "zcode_fallback" as const,
|
||||
},
|
||||
]),
|
||||
]),
|
||||
),
|
||||
);
|
||||
},
|
||||
inspectClient: async (client) => {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -21,13 +21,13 @@ export const managedTargetInputSchema = z.strictObject({
|
||||
.optional(),
|
||||
});
|
||||
|
||||
/** Delivery selection belongs only to artifact inspection, not member/native queries. */
|
||||
/** Delivery selection belongs to artifact and member inspection, not native queries. */
|
||||
export const managedArtifactInputSchema = managedTargetInputSchema.extend({
|
||||
detail: analysisDetailSchema.unwrap().optional(),
|
||||
});
|
||||
|
||||
/** Member inspection may select declaring types before members are decoded. */
|
||||
export const managedMembersInputSchema = managedTargetInputSchema.extend({
|
||||
export const managedMembersInputSchema = managedArtifactInputSchema.extend({
|
||||
type_names: z
|
||||
.array(
|
||||
z
|
||||
@@ -87,7 +87,7 @@ export const MANAGED_TOOL_CONTRACTS = [
|
||||
name: "inspect_managed_members",
|
||||
...toolContractMetadata("inspect_managed_members"),
|
||||
description:
|
||||
"Inspect PE/CLI metadata members, signatures, raw CIL hashes, decoded-instruction-tuple hashes, separately reported exception regions, call edges, field-access anchors, and the #US string literals that ldstr loads, without loading or executing target code. Select type_names to decode only the members of those exact declaring types and the literals their bodies load, which keeps large assemblies such as a base class library within the representation budget. Metadata tokens are reported as build-local coordinates bound to the artifact SHA-256 and MVID; the tuple hash does not resolve them or fully commit control flow.",
|
||||
'Inspect PE/CLI metadata members, signatures, raw CIL hashes, decoded-instruction-tuple hashes, separately reported exception regions, call edges, field-access anchors, and the #US string literals that ldstr loads, without loading or executing target code. Select type_names to decode only the members of those exact declaring types and the literals their bodies load, which keeps large assemblies such as a base class library within the representation budget. Metadata tokens are reported as build-local coordinates bound to the artifact SHA-256 and MVID; the tuple hash does not resolve them or fully commit control flow. Omitted or complete detail returns every collection, including the assembly-wide member_refs and exported_types, which type_names does not narrow. Select detail: "summary" for identity, metadata status, selection with unmatched names, coverage/issues, limitations and per-collection counts; it retains complete Evidence and returns a linked inspect_analysis_view summary. Page original rows with view kind managed-members, normalized_result.parent_evidence_id and collection types/exported_types/fields/methods/member_refs/call_edges/field_accesses/user_strings.',
|
||||
kind: "managed-provider",
|
||||
inputSchema: managedMembersInputSchema,
|
||||
outputSchema: memberOutputSchema,
|
||||
@@ -103,6 +103,14 @@ export const MANAGED_TOOL_CONTRACTS = [
|
||||
type_names: ["System.Threading.Monitor"],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Summarize selected managed types and retain their members",
|
||||
input: {
|
||||
path: "/tmp/mscorlib.dll",
|
||||
type_names: ["System.Threading.Monitor"],
|
||||
detail: "summary",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
artifactInventorySummaryViewResultSchema,
|
||||
machoSummaryViewResultSchema,
|
||||
managedArtifactSummaryViewResultSchema,
|
||||
managedMemberSummaryViewResultSchema,
|
||||
} from "../domain/analysisView/analysisView.js";
|
||||
import { evidenceBundleSummarySchema } from "../domain/evidenceRecordSummary.js";
|
||||
import { BINARY_ARCHITECTURES } from "../domain/binaryTargetTypes.js";
|
||||
@@ -418,7 +419,12 @@ export const managedOutputSchemas: Readonly<Record<string, z.ZodObject>> = {
|
||||
managedArtifactSummaryViewResultSchema,
|
||||
]),
|
||||
),
|
||||
inspect_managed_members: evidenceResultOf(managedMemberInspectionSchema),
|
||||
inspect_managed_members: evidenceResultOf(
|
||||
z.union([
|
||||
managedMemberInspectionSchema,
|
||||
managedMemberSummaryViewResultSchema,
|
||||
]),
|
||||
),
|
||||
inspect_managed_native_boundaries: evidenceResultOf(
|
||||
managedNativeBoundaryInspectionSchema,
|
||||
),
|
||||
|
||||
@@ -49,6 +49,13 @@ import {
|
||||
managedSummarySchema,
|
||||
managedViewContextSchema,
|
||||
} from "./managedArtifactView.js";
|
||||
import {
|
||||
projectManagedMemberView,
|
||||
managedMembersViewRequestSchema,
|
||||
managedMembersViewPageSchema,
|
||||
managedMemberSummarySchema,
|
||||
managedMembersViewContextSchema,
|
||||
} from "./managedMemberView.js";
|
||||
import {
|
||||
projectWasmArtifactView,
|
||||
wasmViewRequestSchema,
|
||||
@@ -208,6 +215,7 @@ export const analysisViewRequestSchema = z.discriminatedUnion("kind", [
|
||||
machoViewRequestSchema,
|
||||
evmViewRequestSchema,
|
||||
managedViewRequestSchema,
|
||||
managedMembersViewRequestSchema,
|
||||
androidViewRequestSchema,
|
||||
apiSurfaceViewSchema,
|
||||
captureViewRequestSchema,
|
||||
@@ -328,6 +336,7 @@ const viewResultBase = {
|
||||
"inspect_macho",
|
||||
"inspect_evm_interface",
|
||||
"inspect_managed_artifact",
|
||||
"inspect_managed_members",
|
||||
"inspect_web_network_capture",
|
||||
"inspect_android_package",
|
||||
"inspect_pcap_capture",
|
||||
@@ -396,6 +405,13 @@ export const managedArtifactSummaryViewResultSchema = z.strictObject({
|
||||
summary: managedSummarySchema,
|
||||
});
|
||||
|
||||
/** Exact member selection, coverage and counts for the selected delivery. */
|
||||
export const managedMemberSummaryViewResultSchema = z.strictObject({
|
||||
...summaryViewResultBase,
|
||||
parent_operation: z.literal("inspect_managed_members"),
|
||||
summary: managedMemberSummarySchema,
|
||||
});
|
||||
|
||||
/** Exact historical capture summary, with no body or member-inventory payload. */
|
||||
export const webNetworkCaptureSummaryViewResultSchema = z.strictObject({
|
||||
...summaryViewResultBase,
|
||||
@@ -446,6 +462,7 @@ export const analysisViewResultSchema = z.discriminatedUnion("kind", [
|
||||
machoSummarySchema,
|
||||
evmSummarySchema,
|
||||
managedSummarySchema,
|
||||
managedMemberSummarySchema,
|
||||
captureSummarySchema,
|
||||
androidPackageSummarySchema,
|
||||
pcapSummarySchema,
|
||||
@@ -529,6 +546,14 @@ export const analysisViewResultSchema = z.discriminatedUnion("kind", [
|
||||
...managedViewContextSchema.shape,
|
||||
parent_operation: z.literal("inspect_managed_artifact"),
|
||||
}),
|
||||
z.strictObject({
|
||||
kind: z.literal("managed-members"),
|
||||
view: managedMembersViewRequestSchema,
|
||||
page: managedMembersViewPageSchema,
|
||||
...viewResultBase,
|
||||
...managedMembersViewContextSchema.shape,
|
||||
parent_operation: z.literal("inspect_managed_members"),
|
||||
}),
|
||||
z.strictObject({
|
||||
kind: z.literal("android"),
|
||||
view: androidViewRequestSchema,
|
||||
@@ -622,6 +647,10 @@ export type UnsignedAnalysisView =
|
||||
Extract<AnalysisViewResult, { readonly kind: "managed" }>,
|
||||
"view_digest"
|
||||
>
|
||||
| Omit<
|
||||
Extract<AnalysisViewResult, { readonly kind: "managed-members" }>,
|
||||
"view_digest"
|
||||
>
|
||||
| Omit<
|
||||
Extract<AnalysisViewResult, { readonly kind: "android" }>,
|
||||
"view_digest"
|
||||
@@ -674,7 +703,7 @@ const unsupportedParent = (
|
||||
new AnalysisUnsupportedTargetError(
|
||||
INSPECT_ANALYSIS_VIEW_OPERATION,
|
||||
parentArtifactPath(parent),
|
||||
`Parent Evidence operation ${JSON.stringify(parent.operation)} does not support analysis views. Use completed binary layout, JavaScript application, native function, artifact inventory/inspection, WASM, Mach-O, EVM, managed artifact, Android package, Dart AOT, or web network capture Evidence.`,
|
||||
`Parent Evidence operation ${JSON.stringify(parent.operation)} does not support analysis views. Use completed binary layout, JavaScript application, native function, artifact inventory/inspection, WASM, Mach-O, EVM, managed artifact or member, Android package, Dart AOT, or web network capture Evidence.`,
|
||||
);
|
||||
|
||||
const parentArtifactPath = (parent: AnalysisViewParent): string => {
|
||||
@@ -704,6 +733,7 @@ const retainedViewProjectors = new Map<string, typeof projectWasmArtifactView>([
|
||||
["inspect_macho", projectMachoView],
|
||||
["inspect_evm_interface", projectEvmInterfaceView],
|
||||
["inspect_managed_artifact", projectManagedArtifactView],
|
||||
["inspect_managed_members", projectManagedMemberView],
|
||||
["inspect_android_package", projectAndroidPackageView],
|
||||
["inspect_dart_aot", projectDartAotView],
|
||||
["inspect_artifact", projectArtifactInventoryView],
|
||||
|
||||
@@ -51,6 +51,24 @@ export const managedViewContextSchema = z.strictObject({
|
||||
source_coverage: managedArtifactInspectionSchema.shape.coverage,
|
||||
});
|
||||
|
||||
/** Token scope and identity limits shared by every managed metadata view. */
|
||||
export const managedViewIdentityUnknowns = (
|
||||
parent: AnalysisViewParent,
|
||||
mvid: string | null,
|
||||
): string[] => [
|
||||
"Metadata tokens are build-local coordinates bound to the recorded artifact SHA-256 and module MVID; this view does not load or execute the assembly.",
|
||||
...(parent.subject == null
|
||||
? [
|
||||
"The parent has no recorded subject; its normalized artifact identity cannot be cross-checked against a subject digest.",
|
||||
]
|
||||
: []),
|
||||
...(mvid === null
|
||||
? [
|
||||
"The module MVID is unknown; tokens cannot establish a cross-observation module match.",
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
/** Project authenticated metadata without reading a path or invoking a provider. */
|
||||
export const projectManagedArtifactView = (
|
||||
parent: AnalysisViewParent,
|
||||
@@ -96,17 +114,7 @@ export const projectManagedArtifactView = (
|
||||
limitations: uniqueSorted([...parent.limitations, ...report.limitations]),
|
||||
unknowns: [
|
||||
"Collection counts and pages cover producer-returned rows; metadata completeness remains the original metadata status and coverage, not an absence claim about unreturned rows.",
|
||||
"Metadata tokens are build-local coordinates bound to the recorded artifact SHA-256 and module MVID; this view does not load or execute the assembly.",
|
||||
...(parent.subject == null
|
||||
? [
|
||||
"The parent has no recorded subject; its normalized artifact identity cannot be cross-checked against a subject digest.",
|
||||
]
|
||||
: []),
|
||||
...(identity_scope.requires_mvid === null
|
||||
? [
|
||||
"The module MVID is unknown; tokens cannot establish a cross-observation module match.",
|
||||
]
|
||||
: []),
|
||||
...managedViewIdentityUnknowns(parent, identity_scope.requires_mvid),
|
||||
],
|
||||
};
|
||||
if (view.kind === "summary") {
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
import { z } from "zod";
|
||||
import type { AnalysisError } from "../analysisErrorBase.js";
|
||||
import { uniqueSorted } from "../canonicalOrdering.js";
|
||||
import { analysisInputErrorFromIssues } from "../inputIssueProjection.js";
|
||||
import {
|
||||
managedMemberInspectionSchema,
|
||||
type ManagedMemberInspection,
|
||||
} from "../managed/managedArtifact.js";
|
||||
import { err, ok, type Result } from "../result.js";
|
||||
import {
|
||||
analysisViewInputError,
|
||||
type AnalysisViewParent,
|
||||
type AnalysisViewRequest,
|
||||
type UnsignedAnalysisView,
|
||||
} from "./analysisView.js";
|
||||
import {
|
||||
completeWithinViewCoverage,
|
||||
pageViewCoverage,
|
||||
} from "./analysisViewCoverage.js";
|
||||
import { managedViewIdentityUnknowns } from "./managedArtifactView.js";
|
||||
|
||||
const member = managedMemberInspectionSchema.shape;
|
||||
|
||||
/** Original member rows; offsets count entries in the returned parent collection. */
|
||||
export const managedMembersViewRequestSchema = z.strictObject({
|
||||
kind: z.literal("managed-members"),
|
||||
collection: z.enum([
|
||||
"types",
|
||||
"exported_types",
|
||||
"fields",
|
||||
"methods",
|
||||
"member_refs",
|
||||
"call_edges",
|
||||
"field_accesses",
|
||||
"user_strings",
|
||||
]),
|
||||
offset: z.number().int().nonnegative().default(0),
|
||||
limit: z.number().int().positive().default(64),
|
||||
});
|
||||
|
||||
const rowsOf = <C extends string, S extends z.ZodArray>(
|
||||
collection: C,
|
||||
rows: S,
|
||||
) => z.strictObject({ collection: z.literal(collection), rows });
|
||||
|
||||
/**
|
||||
* Rows of one collection, keyed by that collection: several row schemas accept
|
||||
* each other's rows after stripping keys, so no union may pick one by order.
|
||||
*/
|
||||
export const managedMembersViewPageSchema = z.discriminatedUnion("collection", [
|
||||
rowsOf("types", member.types),
|
||||
rowsOf("exported_types", member.exported_types.unwrap().unwrap()),
|
||||
rowsOf("fields", member.fields),
|
||||
rowsOf("methods", member.methods),
|
||||
rowsOf("member_refs", member.member_refs),
|
||||
rowsOf("call_edges", member.call_edges),
|
||||
rowsOf("field_accesses", member.field_accesses),
|
||||
rowsOf("user_strings", member.user_strings.unwrap().unwrap().shape.literals),
|
||||
]);
|
||||
|
||||
type ManagedMembersViewPage = z.infer<typeof managedMembersViewPageSchema>;
|
||||
|
||||
const nullableCount = z.number().int().nonnegative().nullable();
|
||||
|
||||
/** Keep identity, selection and coverage; member collections remain retained. */
|
||||
export const managedMemberSummarySchema = z.strictObject({
|
||||
artifact: member.artifact,
|
||||
module: member.module,
|
||||
metadata: member.metadata,
|
||||
identity_scope: member.identity_scope,
|
||||
selection: member.selection.unwrap(),
|
||||
coverage: member.coverage,
|
||||
user_strings: z
|
||||
.strictObject({
|
||||
state: member.user_strings.unwrap().unwrap().shape.state,
|
||||
issues: member.user_strings.unwrap().unwrap().shape.issues,
|
||||
})
|
||||
.nullable(),
|
||||
limitations: member.limitations,
|
||||
reported_counts: z.strictObject({
|
||||
types: z.number().int().nonnegative(),
|
||||
exported_types: nullableCount,
|
||||
fields: z.number().int().nonnegative(),
|
||||
methods: z.number().int().nonnegative(),
|
||||
member_refs: z.number().int().nonnegative(),
|
||||
call_edges: z.number().int().nonnegative(),
|
||||
field_accesses: z.number().int().nonnegative(),
|
||||
user_strings: nullableCount,
|
||||
}),
|
||||
});
|
||||
|
||||
export const managedMembersViewContextSchema = z.strictObject({
|
||||
identity_scope: member.identity_scope,
|
||||
selection: member.selection.unwrap(),
|
||||
source_metadata_status: member.metadata.shape.status,
|
||||
source_coverage: member.coverage,
|
||||
});
|
||||
|
||||
type ManagedMembersViewRequest = z.infer<
|
||||
typeof managedMembersViewRequestSchema
|
||||
>;
|
||||
type MemberCollection = ManagedMembersViewRequest["collection"];
|
||||
|
||||
const slice = <C extends MemberCollection, T>(
|
||||
collection: C,
|
||||
rows: readonly T[] | null | undefined,
|
||||
{ offset, limit }: { readonly offset: number; readonly limit: number },
|
||||
) =>
|
||||
rows == null
|
||||
? null
|
||||
: {
|
||||
page: { collection, rows: rows.slice(offset, offset + limit) },
|
||||
total: rows.length,
|
||||
};
|
||||
|
||||
/** One page of a collection, or null when the parent never examined it. */
|
||||
const collectionPage = (
|
||||
report: ManagedMemberInspection,
|
||||
view: ManagedMembersViewRequest,
|
||||
): { readonly page: ManagedMembersViewPage; readonly total: number } | null => {
|
||||
const { collection } = view;
|
||||
switch (collection) {
|
||||
case "types":
|
||||
return slice(collection, report.types, view);
|
||||
case "exported_types":
|
||||
return slice(collection, report.exported_types, view);
|
||||
case "fields":
|
||||
return slice(collection, report.fields, view);
|
||||
case "methods":
|
||||
return slice(collection, report.methods, view);
|
||||
case "member_refs":
|
||||
return slice(collection, report.member_refs, view);
|
||||
case "call_edges":
|
||||
return slice(collection, report.call_edges, view);
|
||||
case "field_accesses":
|
||||
return slice(collection, report.field_accesses, view);
|
||||
case "user_strings":
|
||||
return slice(collection, report.user_strings?.literals, view);
|
||||
}
|
||||
};
|
||||
|
||||
/** Everything but the member collections, which are replaced by their counts. */
|
||||
const summarizeMembers = (
|
||||
report: ManagedMemberInspection,
|
||||
): z.infer<typeof managedMemberSummarySchema> => {
|
||||
const {
|
||||
types,
|
||||
exported_types,
|
||||
fields,
|
||||
methods,
|
||||
member_refs,
|
||||
call_edges,
|
||||
field_accesses,
|
||||
user_strings,
|
||||
...identity
|
||||
} = report;
|
||||
return {
|
||||
...identity,
|
||||
user_strings:
|
||||
user_strings === null
|
||||
? null
|
||||
: { state: user_strings.state, issues: user_strings.issues },
|
||||
reported_counts: {
|
||||
types: types.length,
|
||||
exported_types: exported_types?.length ?? null,
|
||||
fields: fields.length,
|
||||
methods: methods.length,
|
||||
member_refs: member_refs.length,
|
||||
call_edges: call_edges.length,
|
||||
field_accesses: field_accesses.length,
|
||||
user_strings: user_strings?.literals.length ?? null,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/** Project authenticated member Evidence without reading a path or invoking a provider. */
|
||||
export const projectManagedMemberView = (
|
||||
parent: AnalysisViewParent,
|
||||
view: AnalysisViewRequest,
|
||||
): Result<UnsignedAnalysisView, AnalysisError> => {
|
||||
if (view.kind !== "summary" && view.kind !== "managed-members")
|
||||
return err(
|
||||
analysisViewInputError([
|
||||
{
|
||||
path: ["view"],
|
||||
reason: "invalid_value",
|
||||
message:
|
||||
"Managed member inspection supports summary and managed-members views.",
|
||||
},
|
||||
]),
|
||||
);
|
||||
const parsed = managedMemberInspectionSchema.safeParse(
|
||||
parent.normalizedResult,
|
||||
);
|
||||
if (!parsed.success)
|
||||
return err(
|
||||
analysisInputErrorFromIssues(
|
||||
"inspect_analysis_view",
|
||||
parsed.error.issues.map((issue) => ({
|
||||
...issue,
|
||||
path: ["source", "normalized_result", ...issue.path],
|
||||
})),
|
||||
{ source: { normalized_result: parent.normalizedResult } },
|
||||
{ cause: parsed.error },
|
||||
),
|
||||
);
|
||||
const report = parsed.data;
|
||||
const shared = {
|
||||
parent_evidence_id: parent.evidenceId,
|
||||
parent_operation: "inspect_managed_members" as const,
|
||||
parent_digest: parent.evidenceId.slice(3),
|
||||
artifact: { path: report.artifact.path, sha256: report.artifact.sha256 },
|
||||
limitations: uniqueSorted([...parent.limitations, ...report.limitations]),
|
||||
unknowns: [
|
||||
"Collection counts and pages cover producer-returned rows; member completeness remains the original coverage, not an absence claim about unreturned rows.",
|
||||
...(report.selection === null
|
||||
? []
|
||||
: [
|
||||
"Exported types and member references cover the whole assembly; the other collections cover only the selected declaring types.",
|
||||
]),
|
||||
...managedViewIdentityUnknowns(
|
||||
parent,
|
||||
report.identity_scope.requires_mvid,
|
||||
),
|
||||
],
|
||||
};
|
||||
if (view.kind === "summary") {
|
||||
const summary = summarizeMembers(report);
|
||||
const total = Object.values(summary.reported_counts).reduce<number>(
|
||||
(sum, count) => sum + (count ?? 0),
|
||||
0,
|
||||
);
|
||||
return ok({
|
||||
kind: "summary",
|
||||
view,
|
||||
...shared,
|
||||
summary,
|
||||
coverage: completeWithinViewCoverage(total, total),
|
||||
});
|
||||
}
|
||||
const selected = collectionPage(report, view);
|
||||
if (selected === null)
|
||||
return err(
|
||||
analysisViewInputError([
|
||||
{
|
||||
path: ["view", "collection"],
|
||||
reason: "invalid_value",
|
||||
message: `The parent Evidence did not examine ${view.collection}; repeat inspect_managed_members with the current runtime to record it.`,
|
||||
},
|
||||
]),
|
||||
);
|
||||
const { page, total } = selected;
|
||||
return ok({
|
||||
kind: "managed-members",
|
||||
view,
|
||||
page,
|
||||
...shared,
|
||||
identity_scope: report.identity_scope,
|
||||
selection: report.selection,
|
||||
source_metadata_status: report.metadata.status,
|
||||
source_coverage:
|
||||
view.collection === "user_strings" && report.user_strings !== null
|
||||
? {
|
||||
state: report.user_strings.state,
|
||||
issues: report.user_strings.issues,
|
||||
}
|
||||
: report.coverage,
|
||||
coverage: pageViewCoverage(view.offset, page.rows.length, total),
|
||||
});
|
||||
};
|
||||
@@ -259,30 +259,6 @@ describe("historical source graph", () => {
|
||||
});
|
||||
|
||||
describe("partial historical source graphs", () => {
|
||||
it("keeps unknown-entry diagnostics outside the root commitment", () => {
|
||||
const base = graphInput();
|
||||
base.inventory_state = "partial";
|
||||
base.entries[2] = {
|
||||
path: "src/package.json",
|
||||
kind: "unknown",
|
||||
classifications: ["unknown"],
|
||||
limitations: ["Permission denied at /private/first"],
|
||||
};
|
||||
const graph = createHistoricalSourceGraph(base);
|
||||
const changedMessage = structuredClone(base);
|
||||
changedMessage.entries[2] = {
|
||||
path: "src/package.json",
|
||||
kind: "unknown",
|
||||
classifications: ["unknown"],
|
||||
limitations: ["Permission denied at /private/second"],
|
||||
};
|
||||
const revised = createHistoricalSourceGraph(changedMessage);
|
||||
expect(revised.root_sha256).toBe(graph.root_sha256);
|
||||
expect(computeHistoricalSourceGraphSha256(revised)).not.toBe(
|
||||
computeHistoricalSourceGraphSha256(graph),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps an unreadable symlink target unknown and out of root identity", () => {
|
||||
const base = graphInput();
|
||||
base.inventory_state = "partial";
|
||||
|
||||
@@ -111,10 +111,6 @@ const sourceEntrySchema = z.union([
|
||||
sourceFileSchema,
|
||||
sourceDirectorySchema,
|
||||
sourceSymlinkSchema,
|
||||
z.strictObject({
|
||||
...entryBaseShape,
|
||||
kind: z.literal("unknown"),
|
||||
}),
|
||||
]);
|
||||
|
||||
const sourceRelationshipSchema = z.strictObject({
|
||||
@@ -365,8 +361,7 @@ const checkCompleteInventory = (
|
||||
entry.limitations.length > 0 ||
|
||||
(entry.kind === "file" && entry.content_state !== "hashed") ||
|
||||
(entry.kind === "directory" && entry.tree_state !== "enumerated") ||
|
||||
(entry.kind === "symlink" && entry.target_state !== "internal") ||
|
||||
entry.kind === "unknown",
|
||||
(entry.kind === "symlink" && entry.target_state !== "internal"),
|
||||
);
|
||||
const partialRelationship = graph.relationships.some(
|
||||
({ parse_state, resolution }) =>
|
||||
@@ -483,11 +478,6 @@ const rootCommitment = (
|
||||
target: entry.target,
|
||||
target_state: entry.target_state,
|
||||
};
|
||||
case "unknown":
|
||||
return {
|
||||
kind: entry.kind,
|
||||
path: entry.path,
|
||||
};
|
||||
}
|
||||
}),
|
||||
exclusions,
|
||||
|
||||
@@ -81,26 +81,6 @@ describe("committed historical-source summaries", () => {
|
||||
expect(graph).toEqual(original);
|
||||
});
|
||||
|
||||
it("counts entries of unknown kind separately from symlinks", () => {
|
||||
const graph = createHistoricalSourceGraph(
|
||||
input([
|
||||
{
|
||||
path: "deep",
|
||||
kind: "unknown",
|
||||
classifications: ["unknown"],
|
||||
limitations: ["lstat: ENAMETOOLONG"],
|
||||
},
|
||||
]),
|
||||
);
|
||||
expect(createHistoricalSourceSummary(graph, 1).counts).toMatchObject({
|
||||
entries: 1,
|
||||
files: 0,
|
||||
directories: 0,
|
||||
symlinks: 0,
|
||||
unknown: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it("sums known bytes losslessly beyond the JSON safe integer range", () => {
|
||||
const graph = createHistoricalSourceGraph(
|
||||
input(
|
||||
|
||||
@@ -29,7 +29,6 @@ export const createHistoricalSourceSummary = (
|
||||
files: 0,
|
||||
directories: 0,
|
||||
symlinks: 0,
|
||||
unknown: 0,
|
||||
relationships: graph.relationships.length,
|
||||
parse_failures: graph.parse_failures.length,
|
||||
exclusions: graph.exclusions.length,
|
||||
@@ -60,8 +59,7 @@ export const createHistoricalSourceSummary = (
|
||||
} else if (entry.kind === "directory") {
|
||||
counts.directories += 1;
|
||||
treeStates[entry.tree_state] += 1;
|
||||
} else if (entry.kind === "symlink") counts.symlinks += 1;
|
||||
else counts.unknown += 1;
|
||||
} else counts.symlinks += 1;
|
||||
// Canonical entries already use Unicode code-point order; filtering preserves it.
|
||||
if (entry.path.split("/").length <= outlineDepth)
|
||||
outline.push(outlineEntry(entry));
|
||||
|
||||
@@ -5,5 +5,5 @@ export const PACKAGE_METADATA = {
|
||||
serverSdkVersion: "2.3.1",
|
||||
clientSdkVersion: "2.3.1",
|
||||
coreSdkVersion: "2.3.1",
|
||||
skillVersion: "42",
|
||||
skillVersion: "41",
|
||||
} as const;
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
import type {
|
||||
PendingDirectory,
|
||||
ReferenceSourceEntry,
|
||||
ReferenceSourceExclusionKind,
|
||||
ReferenceSourceEntryKind,
|
||||
ReferenceSourceResult,
|
||||
TraversalState,
|
||||
} from "./ReferenceSourceReaderTypes.js";
|
||||
@@ -201,13 +201,6 @@ const processEntry = async (
|
||||
const metadata = await readMetadata(absolute);
|
||||
if (isAborted(state.signal)) return { ok: false, error: cancelled() };
|
||||
if (!metadata.ok) {
|
||||
const excluded = applyExclusion(state.shouldExclude, path, "unknown");
|
||||
if (!excluded.ok)
|
||||
return {
|
||||
ok: false,
|
||||
error: failure("io", "Reference source exclusion check failed"),
|
||||
};
|
||||
if (excluded.value) return { ok: true, value: undefined };
|
||||
state.entries.push(entryFailure(path, "unknown", "io", metadata.message));
|
||||
return { ok: true, value: undefined };
|
||||
}
|
||||
@@ -378,10 +371,10 @@ const readSymlinkTarget = async (
|
||||
|
||||
const applyExclusion = (
|
||||
shouldExclude:
|
||||
| ((path: string, kind: ReferenceSourceExclusionKind) => boolean)
|
||||
| ((path: string, kind: ReferenceSourceEntryKind) => boolean)
|
||||
| undefined,
|
||||
path: string,
|
||||
kind: ReferenceSourceExclusionKind,
|
||||
kind: ReferenceSourceEntryKind,
|
||||
): { readonly ok: true; readonly value: boolean } | { readonly ok: false } => {
|
||||
try {
|
||||
return { ok: true, value: shouldExclude?.(path, kind) === true };
|
||||
|
||||
@@ -29,9 +29,6 @@ const FILESYSTEM_ERROR_CODES = new Set([
|
||||
"EISDIR",
|
||||
"ELOOP",
|
||||
"EMFILE",
|
||||
// A traversed child can exceed the platform's absolute path limit even
|
||||
// though the selected root and every component are valid.
|
||||
"ENAMETOOLONG",
|
||||
"ENFILE",
|
||||
"ENOENT",
|
||||
"ENOTDIR",
|
||||
@@ -47,8 +44,6 @@ export const filesystemFailureDetail = (
|
||||
): string | undefined => {
|
||||
if (!(cause instanceof Error)) return undefined;
|
||||
const code: unknown = Reflect.get(cause, "code");
|
||||
if (code === "ENAMETOOLONG")
|
||||
return `${operation}: ENAMETOOLONG (entry path exceeds the operating system path limit)`;
|
||||
return typeof code === "string" && FILESYSTEM_ERROR_CODES.has(code)
|
||||
? `${operation}: ${cause.message}`
|
||||
: undefined;
|
||||
|
||||
@@ -10,17 +10,11 @@ export type ReferenceSourceEntryKind =
|
||||
| "symlink"
|
||||
| "other";
|
||||
|
||||
/**
|
||||
* Kind offered to exclusion predicates. `unknown` marks an entry whose
|
||||
* metadata could not be read, so its concrete kind cannot be claimed.
|
||||
*/
|
||||
export type ReferenceSourceExclusionKind = ReferenceSourceEntryKind | "unknown";
|
||||
|
||||
export interface ReferenceSourceReaderOptions {
|
||||
readonly signal?: AbortSignal;
|
||||
readonly shouldExclude?: (
|
||||
path: string,
|
||||
kind: ReferenceSourceExclusionKind,
|
||||
kind: ReferenceSourceEntryKind,
|
||||
) => boolean;
|
||||
}
|
||||
|
||||
@@ -90,7 +84,7 @@ export type TraversalState = {
|
||||
readonly signal?: AbortSignal;
|
||||
readonly shouldExclude?: (
|
||||
path: string,
|
||||
kind: ReferenceSourceExclusionKind,
|
||||
kind: ReferenceSourceEntryKind,
|
||||
) => boolean;
|
||||
readonly entries: ReferenceSourceEntry[];
|
||||
readonly pending: PendingDirectory[];
|
||||
|
||||
@@ -16,6 +16,15 @@ import { registerEvidenceTools } from "./registerEvidenceTools.js";
|
||||
import { runManagedProviderExecution } from "../composition/directAnalysis.js";
|
||||
import { isManagedToolName } from "../contracts/managed/managedToolContracts.js";
|
||||
|
||||
/** Operations whose caller may select a retained summary instead of complete delivery. */
|
||||
const summarizesRetainedEvidence = (
|
||||
operation: string,
|
||||
parameters: Readonly<Record<string, unknown>>,
|
||||
): boolean =>
|
||||
(operation === "inspect_managed_artifact" ||
|
||||
operation === "inspect_managed_members") &&
|
||||
parameters.detail === "summary";
|
||||
|
||||
/** Register execution-free managed PE/CLI inspection. */
|
||||
export const registerManagedTools = (
|
||||
server: EvidenceMcpServer,
|
||||
@@ -34,11 +43,8 @@ export const registerManagedTools = (
|
||||
execute: async (operation, parameters, executionOptions) => {
|
||||
if (!isManagedToolName(operation))
|
||||
return analysis.execute(operation, parameters, executionOptions);
|
||||
const { detail: _detail, ...artifactParameters } = parameters;
|
||||
if (
|
||||
operation === "inspect_managed_artifact" &&
|
||||
parameters.detail === "summary"
|
||||
) {
|
||||
const { detail: _detail, ...providerParameters } = parameters;
|
||||
if (summarizesRetainedEvidence(operation, parameters)) {
|
||||
const ready = requireAnalysisViewRetention(operation, {
|
||||
recordEvidence: options.recordEvidence,
|
||||
evidenceById: options.evidenceById,
|
||||
@@ -69,9 +75,7 @@ export const registerManagedTools = (
|
||||
path,
|
||||
operation,
|
||||
executionOptions?.signal,
|
||||
operation === "inspect_managed_artifact"
|
||||
? artifactParameters
|
||||
: parameters,
|
||||
providerParameters,
|
||||
);
|
||||
if (execution.ok && typeof requestedPath === "string")
|
||||
selectedManagedPath = requestedPath;
|
||||
@@ -82,8 +86,7 @@ export const registerManagedTools = (
|
||||
...options,
|
||||
analysisForAdmission,
|
||||
projectEvidence: (operation, parameters, evidence) =>
|
||||
operation === "inspect_managed_artifact" &&
|
||||
parameters.detail === "summary"
|
||||
summarizesRetainedEvidence(operation, parameters)
|
||||
? summarizeRetainedAnalysis(evidence, {
|
||||
recordEvidence: options.recordEvidence,
|
||||
evidenceById: options.evidenceById,
|
||||
|
||||
@@ -279,44 +279,26 @@ it("preserves bounded instruction coverage separately from using-node coverage",
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
"not-a-dex-descriptor",
|
||||
"(Lbad(name;)V",
|
||||
"(Lbad)name;)V",
|
||||
"()Lbad(name;",
|
||||
"()Lbad)name;",
|
||||
])(
|
||||
"rejects malformed DEX prototype %s at the adapter boundary",
|
||||
async (descriptor) => {
|
||||
const invalidSummary = {
|
||||
...summary,
|
||||
methods: summary.methods.map((method) => ({
|
||||
...method,
|
||||
dex_descriptor: descriptor,
|
||||
})),
|
||||
};
|
||||
await expect(
|
||||
analyzeJadxRequest(
|
||||
{
|
||||
json: () => Promise.resolve(invalidSummary),
|
||||
text: () => Promise.reject(new Error("unexpected text call")),
|
||||
},
|
||||
{
|
||||
operation: "inspect_android_class",
|
||||
input: { path: "/fixture.apk", class_name: "fixture.Target" },
|
||||
},
|
||||
{} as never,
|
||||
engine,
|
||||
),
|
||||
).rejects.toMatchObject({
|
||||
_tag: "AnalysisOutputError",
|
||||
cause: {
|
||||
issues: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
path: ["methods", 0, "dex_descriptor"],
|
||||
}),
|
||||
]),
|
||||
it("rejects malformed method descriptors at the Android adapter boundary", async () => {
|
||||
const invalidSummary = {
|
||||
...summary,
|
||||
methods: summary.methods.map((method) => ({
|
||||
...method,
|
||||
dex_descriptor: "not-a-dex-descriptor",
|
||||
})),
|
||||
};
|
||||
await expect(
|
||||
analyzeJadxRequest(
|
||||
{
|
||||
json: () => Promise.resolve(invalidSummary),
|
||||
text: () => Promise.reject(new Error("unexpected text call")),
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
{
|
||||
operation: "inspect_android_class",
|
||||
input: { path: "/fixture.apk", class_name: "fixture.Target" },
|
||||
},
|
||||
{} as never,
|
||||
engine,
|
||||
),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
@@ -133,88 +133,3 @@ describe("reference import native entry failure details", () => {
|
||||
).toMatchObject({ kind: "file", content_state: "hashed", limitations: [] });
|
||||
});
|
||||
});
|
||||
|
||||
describe("reference import paths beyond the platform limit", () => {
|
||||
it.skipIf(process.platform === "win32")(
|
||||
"reports an entry beyond the absolute path limit as an I/O failure",
|
||||
async () => {
|
||||
const root = await createTestTempDirectory("rea-import-deep-path-");
|
||||
await writeFile(join(root, "kept.txt"), "k");
|
||||
// Relative creation lets the tree exceed PATH_MAX (1024 on macOS, 4096
|
||||
// on Linux) while each 200-byte component stays below NAME_MAX.
|
||||
const deepTree = (action: "create" | "remove") =>
|
||||
promisify(execFile)(process.execPath, [
|
||||
"-e",
|
||||
`const fs = require("node:fs");
|
||||
const names = Array.from({ length: 24 }, (_, i) => "d" + String(i).padStart(2, "0") + "x".repeat(197));
|
||||
process.chdir(${JSON.stringify(root)});
|
||||
if (${JSON.stringify(action)} === "create") {
|
||||
for (const name of names) { fs.mkdirSync(name); process.chdir(name); }
|
||||
fs.writeFileSync("deep.txt", "d");
|
||||
} else {
|
||||
for (const name of names) process.chdir(name);
|
||||
fs.rmSync("deep.txt");
|
||||
for (const name of [...names].reverse()) { process.chdir(".."); fs.rmdirSync(name); }
|
||||
}`,
|
||||
]);
|
||||
await deepTree("create");
|
||||
try {
|
||||
const read = await readReferenceSource(root);
|
||||
if (!read.ok) throw new Error(read.error.message);
|
||||
expect(read.value.entries).toContainEqual(
|
||||
expect.objectContaining({ path: "kept.txt", status: "read" }),
|
||||
);
|
||||
const failed = read.value.entries.find(
|
||||
(entry) => entry.status === "failed",
|
||||
);
|
||||
expect(failed).toMatchObject({
|
||||
status: "failed",
|
||||
code: "io",
|
||||
message: expect.stringContaining("ENAMETOOLONG"),
|
||||
});
|
||||
const filteredRead = await readReferenceSource(root, {
|
||||
shouldExclude: (path, kind) =>
|
||||
path === failed?.path && kind === "directory",
|
||||
});
|
||||
if (!filteredRead.ok) throw new Error(filteredRead.error.message);
|
||||
expect(
|
||||
filteredRead.value.entries.find(
|
||||
(entry) => entry.path === failed?.path,
|
||||
),
|
||||
).toMatchObject({ status: "failed", kind: "unknown", code: "io" });
|
||||
const imported = await importTree(root);
|
||||
if (!imported.ok) throw new Error(imported.error.message);
|
||||
expect(imported.value.entries).toContainEqual(
|
||||
expect.objectContaining({
|
||||
path: "kept.txt",
|
||||
content_state: "hashed",
|
||||
}),
|
||||
);
|
||||
const deep = imported.value.entries.find(
|
||||
(entry) => entry.path === failed?.path,
|
||||
);
|
||||
expect(deep?.limitations.join(" ")).toContain(
|
||||
failed?.status === "failed" ? failed.message : "missing failure",
|
||||
);
|
||||
// A directory-only secret rule still applies when the kind is unknown.
|
||||
const secretPattern = `${failed?.path ?? "missing"}/`;
|
||||
const secret = await importReferenceSource({
|
||||
root,
|
||||
caller: "native-entry-failure-detail-test",
|
||||
policy: { secretPatterns: [secretPattern] },
|
||||
});
|
||||
if (!secret.ok) throw new Error(secret.error.message);
|
||||
expect(
|
||||
secret.value.entries.some(({ path }) => path === failed?.path),
|
||||
).toBe(false);
|
||||
expect(secret.value.exclusions).toContainEqual({
|
||||
path: failed?.path,
|
||||
reason: "configured-secret",
|
||||
pattern: secretPattern,
|
||||
});
|
||||
} finally {
|
||||
await deepTree("remove");
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -13,10 +13,7 @@ import {
|
||||
} from "../../../src/application/SetupClientConfiguration.js";
|
||||
import { skillDestinations } from "../../../src/application/SetupSkill.js";
|
||||
import { supportedClients } from "../../../src/application/SupportedClients.js";
|
||||
import {
|
||||
runUninstall,
|
||||
systemUninstallHost,
|
||||
} from "../../../src/application/Uninstall.js";
|
||||
import { systemUninstallHost } from "../../../src/application/Uninstall.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { clearClientLocationEnvironment } from "../../fixtures/clientEnvironment.js";
|
||||
|
||||
@@ -260,91 +257,6 @@ describe("ZCode .agents fallback preservation", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("ZCode fallback uninstall", () => {
|
||||
const owned = { command: command[0], args: command.slice(1) };
|
||||
const writeFallback = async (home: string, servers: object) => {
|
||||
await mkdir(join(home, ".agents"), { recursive: true });
|
||||
const text = `${JSON.stringify({ ui: { keep: true }, mcpServers: servers }, null, 2)}\n`;
|
||||
await writeFile(fallbackPath(home), text);
|
||||
return text;
|
||||
};
|
||||
|
||||
it.each(["comment", "trailing-comma", "bom"])(
|
||||
"fails closed on strict fallback syntax: %s",
|
||||
async (mode) => {
|
||||
const { home } = await zcodeClient();
|
||||
const valid = await writeFallback(home, {
|
||||
rea: owned,
|
||||
other: { command: "other-server" },
|
||||
});
|
||||
const original =
|
||||
mode === "comment"
|
||||
? `// unsupported comment\n${valid}`
|
||||
: mode === "bom"
|
||||
? `\uFEFF${valid}`
|
||||
: valid.replace(/\}\s*$/u, ",}");
|
||||
await writeFile(fallbackPath(home), original);
|
||||
const result = await runUninstall(false, systemUninstallHost(home));
|
||||
expect(result.items).toContainEqual(
|
||||
expect.objectContaining({ name: "zcode", status: "failed" }),
|
||||
);
|
||||
expect(await readFile(fallbackPath(home), "utf8")).toBe(original);
|
||||
await expect(
|
||||
readFile(`${fallbackPath(home)}.rea.backup`),
|
||||
).rejects.toMatchObject({ code: "ENOENT" });
|
||||
},
|
||||
);
|
||||
|
||||
it("removes the REA-owned fallback entry with the native registration", async () => {
|
||||
const { home, client } = await zcodeClient();
|
||||
await writeFile(
|
||||
client.configPath,
|
||||
JSON.stringify({ mcp: { servers: { rea: owned } } }),
|
||||
);
|
||||
const original = await writeFallback(home, {
|
||||
rea: owned,
|
||||
other: { command: "other-server" },
|
||||
});
|
||||
|
||||
const result = await runUninstall(false, systemUninstallHost(home));
|
||||
|
||||
expect(result.status).toBe("complete");
|
||||
expect(result.items.find(({ name }) => name === "zcode")?.status).toBe(
|
||||
"removed",
|
||||
);
|
||||
const native = JSON.parse(await readFile(client.configPath, "utf8"));
|
||||
expect(native.mcp.servers).toEqual({});
|
||||
const fallback = JSON.parse(await readFile(fallbackPath(home), "utf8"));
|
||||
expect(fallback).toEqual({
|
||||
ui: { keep: true },
|
||||
mcpServers: { other: { command: "other-server" } },
|
||||
});
|
||||
expect(await readFile(`${fallbackPath(home)}.rea.backup`, "utf8")).toBe(
|
||||
original,
|
||||
);
|
||||
});
|
||||
|
||||
it("removes a fallback-only REA entry and keeps a foreign rea entry", async () => {
|
||||
for (const [servers, expected] of [
|
||||
[{ rea: owned }, {}],
|
||||
[
|
||||
{ rea: { command: "someone-else" } },
|
||||
{ rea: { command: "someone-else" } },
|
||||
],
|
||||
] as const) {
|
||||
const { home } = await zcodeClient();
|
||||
await writeFallback(home, servers);
|
||||
|
||||
expect(
|
||||
(await runUninstall(false, systemUninstallHost(home))).status,
|
||||
).toBe("complete");
|
||||
expect(
|
||||
JSON.parse(await readFile(fallbackPath(home), "utf8")).mcpServers,
|
||||
).toEqual(expected);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("ZCode fallback admission", () => {
|
||||
it.each([{ PORT: 123 }, { REA_MCP_INPUT_SCHEMA_PROFILE: "unknown" }])(
|
||||
"refuses malformed fallback REA environment %j before native writes",
|
||||
|
||||
@@ -49,16 +49,13 @@ it("returns a first-call overview and pages the unchanged source after its file
|
||||
const ajv = new Ajv2020({ strict: false, validateFormats: false });
|
||||
expect(ajv.validateSchema(definition.inputSchema)).toBe(true);
|
||||
expect(ajv.validateSchema(outputSchema)).toBe(true);
|
||||
for (const name of [
|
||||
"inspect_managed_members",
|
||||
"inspect_managed_native_boundaries",
|
||||
])
|
||||
expect(
|
||||
ajv.validate(catalog.find((x) => x.name === name)!.inputSchema, {
|
||||
path,
|
||||
detail: "summary",
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
ajv.validate(
|
||||
catalog.find((x) => x.name === "inspect_managed_native_boundaries")!
|
||||
.inputSchema,
|
||||
{ path, detail: "summary" },
|
||||
),
|
||||
).toBe(false);
|
||||
const reply = await client.callTool({
|
||||
name: "inspect_managed_artifact",
|
||||
arguments: { path, detail: "summary" },
|
||||
@@ -239,11 +236,15 @@ it("refuses selected delivery before file access when the server cannot retain E
|
||||
const [a, b] = InMemoryTransport.createLinkedPair();
|
||||
await server.connect(b);
|
||||
await client.connect(a);
|
||||
const rejected = await client.callTool({
|
||||
name: "inspect_managed_artifact",
|
||||
arguments: { path: join(directory, "absent.dll"), detail: "summary" },
|
||||
});
|
||||
expect(parseMcpToolError(rejected).error.code).toBe("capability_unavailable");
|
||||
for (const name of ["inspect_managed_artifact", "inspect_managed_members"]) {
|
||||
const rejected = await client.callTool({
|
||||
name,
|
||||
arguments: { path: join(directory, "absent.dll"), detail: "summary" },
|
||||
});
|
||||
expect(parseMcpToolError(rejected).error.code).toBe(
|
||||
"capability_unavailable",
|
||||
);
|
||||
}
|
||||
const complete = await client.callTool({
|
||||
name: "inspect_managed_artifact",
|
||||
arguments: { path },
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
import { writeFile, unlink } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
import { expect, it, onTestFinished } from "vitest";
|
||||
import { composeBinarySession } from "../../../src/application/binary/BinarySessionComposition.js";
|
||||
import { AnalysisProviderRegistry } from "../../../src/application/binary/AnalysisProviderRegistry.js";
|
||||
import { inspectAnalysisView } from "../../../src/application/analysisView/AnalysisViewService.js";
|
||||
import { ManagedStaticProvider } from "../../../src/dotnet/ManagedStaticProvider.js";
|
||||
import { buildManagedPeFixture } from "../../../src/dotnet/ManagedPe.fixture.js";
|
||||
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
|
||||
import {
|
||||
analysisViewResultSchema,
|
||||
managedMemberSummaryViewResultSchema,
|
||||
} from "../../../src/domain/analysisView/analysisView.js";
|
||||
import { managedMemberInspectionSchema } from "../../../src/domain/managed/managedArtifact.js";
|
||||
import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
const COLLECTIONS = [
|
||||
"types",
|
||||
"exported_types",
|
||||
"fields",
|
||||
"methods",
|
||||
"member_refs",
|
||||
"call_edges",
|
||||
"field_accesses",
|
||||
"user_strings",
|
||||
] as const;
|
||||
|
||||
const createHarness = async () => {
|
||||
const session = composeBinarySession(new AnalysisProviderRegistry([]), [
|
||||
new ManagedStaticProvider(),
|
||||
]);
|
||||
const harness = await createApplicationMcpHarness({ session });
|
||||
onTestFinished(harness.close);
|
||||
return harness;
|
||||
};
|
||||
|
||||
it("summarizes selected members and pages every original collection after the file disappears", async () => {
|
||||
const directory = await createTestTempDirectory("rea-member-summary-");
|
||||
const path = join(directory, "fixture.dll");
|
||||
await writeFile(path, buildManagedPeFixture({ methodRowCount: 2 }));
|
||||
const { client, session } = await createHarness();
|
||||
const definition = (await client.listTools()).tools.find(
|
||||
(x) => x.name === "inspect_managed_members",
|
||||
);
|
||||
if (definition?.outputSchema === undefined)
|
||||
throw new Error("Managed member schemas are absent");
|
||||
const outputSchema: Record<string, unknown> = definition.outputSchema;
|
||||
const ajv = new Ajv2020({ strict: false, validateFormats: false });
|
||||
expect(ajv.validateSchema(definition.inputSchema)).toBe(true);
|
||||
expect(ajv.validateSchema(outputSchema)).toBe(true);
|
||||
const type_names = ["Fixture.Program", "Missing.Type"];
|
||||
expect(
|
||||
ajv.validate(definition.inputSchema, {
|
||||
path,
|
||||
type_names,
|
||||
detail: "summary",
|
||||
}),
|
||||
).toBe(true);
|
||||
const reply = await client.callTool({
|
||||
name: "inspect_managed_members",
|
||||
arguments: { path, type_names, detail: "summary" },
|
||||
});
|
||||
expect(reply.isError).not.toBe(true);
|
||||
expect(
|
||||
ajv.validate(outputSchema, reply.structuredContent),
|
||||
JSON.stringify(ajv.errors),
|
||||
).toBe(true);
|
||||
const receipt = parseEvidence(reply.structuredContent);
|
||||
const view = managedMemberSummaryViewResultSchema.parse(
|
||||
receipt.normalized_result,
|
||||
);
|
||||
const parent = session.evidenceById(view.parent_evidence_id);
|
||||
if (parent === undefined)
|
||||
throw new Error("Complete member source was not retained");
|
||||
expect(parent.operation).toBe("inspect_managed_members");
|
||||
expect(receipt.evidence_links).toEqual([parent.evidence_id]);
|
||||
const report = managedMemberInspectionSchema.parse(parent.normalized_result);
|
||||
expect(view.summary.selection).toEqual({
|
||||
type_names,
|
||||
unmatched_type_names: ["Missing.Type"],
|
||||
});
|
||||
expect(view.summary.coverage).toEqual(report.coverage);
|
||||
expect(view.summary.identity_scope).toEqual(report.identity_scope);
|
||||
expect(view.summary).not.toHaveProperty("methods");
|
||||
expect(report.methods.length).toBeGreaterThan(0);
|
||||
expect(report.member_refs.length).toBeGreaterThan(0);
|
||||
for (const detail of [undefined, "complete"]) {
|
||||
const complete = parseEvidence(
|
||||
(
|
||||
await client.callTool({
|
||||
name: "inspect_managed_members",
|
||||
arguments: { path, type_names, ...(detail ? { detail } : {}) },
|
||||
})
|
||||
).structuredContent,
|
||||
);
|
||||
expect(complete.operation).toBe("inspect_managed_members");
|
||||
expect(complete.normalized_result).toEqual(parent.normalized_result);
|
||||
}
|
||||
const original = JSON.stringify(parent);
|
||||
await unlink(path);
|
||||
for (const collection of COLLECTIONS) {
|
||||
const rows =
|
||||
collection === "user_strings"
|
||||
? report.user_strings?.literals
|
||||
: report[collection];
|
||||
if (rows == null) throw new Error(`${collection} was not examined`);
|
||||
expect(view.summary.reported_counts[collection]).toBe(rows.length);
|
||||
const paged: unknown[] = [];
|
||||
for (let offset: number | null = 0; offset !== null;) {
|
||||
const result = await client.callTool({
|
||||
name: "inspect_analysis_view",
|
||||
arguments: {
|
||||
source: {
|
||||
kind: "retained-evidence",
|
||||
evidence_id: parent.evidence_id,
|
||||
},
|
||||
view: { kind: "managed-members", collection, offset, limit: 1 },
|
||||
},
|
||||
});
|
||||
expect(result.isError).not.toBe(true);
|
||||
const page = analysisViewResultSchema.parse(
|
||||
parseEvidence(result.structuredContent).normalized_result,
|
||||
);
|
||||
if (page.kind !== "managed-members")
|
||||
throw new Error("Unexpected page kind");
|
||||
expect(page.page.collection).toBe(collection);
|
||||
expect(page.selection).toEqual(report.selection);
|
||||
expect(page.coverage.total).toBe(rows.length);
|
||||
paged.push(...page.page.rows);
|
||||
offset = page.coverage.next_offset;
|
||||
}
|
||||
// Method rows carry body facts that a looser row schema would strip.
|
||||
expect(paged).toEqual(rows);
|
||||
}
|
||||
expect(JSON.stringify(session.evidenceById(parent.evidence_id))).toBe(
|
||||
original,
|
||||
);
|
||||
const portable = inspectAnalysisView({
|
||||
source: { kind: "inline", evidence: parent },
|
||||
view: { kind: "summary" },
|
||||
});
|
||||
if (!portable.ok) throw portable.error;
|
||||
expect(portable.value.normalized_result).toEqual(view);
|
||||
}, 30000);
|
||||
|
||||
it("keeps collections that historical member Evidence never examined unknown", async () => {
|
||||
const directory = await createTestTempDirectory("rea-member-historical-");
|
||||
const path = join(directory, "fixture.dll");
|
||||
await writeFile(path, buildManagedPeFixture());
|
||||
const { client } = await createHarness();
|
||||
const parent = parseEvidence(
|
||||
(
|
||||
await client.callTool({
|
||||
name: "inspect_managed_members",
|
||||
arguments: { path },
|
||||
})
|
||||
).structuredContent,
|
||||
);
|
||||
const {
|
||||
exported_types: _exported,
|
||||
user_strings: _strings,
|
||||
selection: _selection,
|
||||
...recorded
|
||||
} = managedMemberInspectionSchema.parse(parent.normalized_result);
|
||||
const historical = createEvidence(undefined, parent.provider, {
|
||||
operation: parent.operation,
|
||||
parameters: parent.parameters,
|
||||
result: recorded,
|
||||
limitations: parent.limitations,
|
||||
authority: parent.authority,
|
||||
});
|
||||
const summary = inspectAnalysisView({
|
||||
source: { kind: "inline", evidence: historical },
|
||||
view: { kind: "summary" },
|
||||
});
|
||||
if (!summary.ok) throw summary.error;
|
||||
const view = managedMemberSummaryViewResultSchema.parse(
|
||||
summary.value.normalized_result,
|
||||
);
|
||||
expect(view.summary.reported_counts.exported_types).toBeNull();
|
||||
expect(view.summary.reported_counts.user_strings).toBeNull();
|
||||
expect(view.summary.user_strings).toBeNull();
|
||||
expect(view.summary.selection).toBeNull();
|
||||
expect(view.unknowns.join(" ")).toContain("no recorded subject");
|
||||
for (const collection of ["exported_types", "user_strings"] as const)
|
||||
expect(
|
||||
inspectAnalysisView({
|
||||
source: { kind: "inline", evidence: historical },
|
||||
view: { kind: "managed-members", collection, offset: 0, limit: 1 },
|
||||
}),
|
||||
).toMatchObject({ ok: false, error: { _tag: "AnalysisInputError" } });
|
||||
expect(
|
||||
inspectAnalysisView({
|
||||
source: { kind: "inline", evidence: historical },
|
||||
view: { kind: "managed", collection: "attributes", offset: 0, limit: 1 },
|
||||
}),
|
||||
).toMatchObject({ ok: false, error: { _tag: "AnalysisInputError" } });
|
||||
}, 30000);
|
||||
Reference in New Issue
Block a user