Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1784ca940a | ||
|
|
36eca26b38 | ||
|
|
96a46ada0a | ||
|
|
b4fc0ecaa1 | ||
|
|
64c71880ae | ||
|
|
fe64fd65d9 | ||
|
|
ae0f725fad | ||
|
|
a6132be054 | ||
|
|
cbc0dd1e7c | ||
|
|
eb8f6491a8 | ||
|
|
6aa9afb549 | ||
|
|
5b172b1042 | ||
|
|
06b047a092 | ||
|
|
161ed91571 | ||
|
|
6f0ef16050 | ||
|
|
bda4ada7fb | ||
|
|
35a4e18d30 | ||
|
|
c67530535a | ||
|
|
701efd5f90 | ||
|
|
e45da24297 | ||
|
|
dca14900c4 |
@@ -28,7 +28,7 @@ intentionally not included. -->
|
||||
- Provider, bridge, target-format, or platform compatibility:
|
||||
- Evidence, artifact, provenance, or reconstruction contract:
|
||||
- Process execution, authorization, cleanup, or containment impact:
|
||||
- Generated metadata (`docs/product-catalog.json`), package, or installation impact:
|
||||
- Generated metadata (`docs/product-catalog.json`), package, or installation impact (the regen-generated bot pushes tracked generated files; never hand-edit them):
|
||||
|
||||
## Evidence and regression coverage
|
||||
|
||||
|
||||
@@ -102,6 +102,8 @@ jobs:
|
||||
with:
|
||||
name: rea-dist
|
||||
path: dist
|
||||
- name: Generate source catalog from downloaded build
|
||||
run: node scripts/generate-mcp-tool-catalog.mjs
|
||||
- run: npm run test:ci:shard:run -- --shard=${{ matrix.shard }}
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() }}
|
||||
@@ -314,6 +316,8 @@ jobs:
|
||||
run: npm run test:focused -- tests/boundary/cli/nativePlistCli.test.ts tests/boundary/cli/nativePlistPrototypeKeyCli.test.ts tests/boundary/cli/nativePlistSelectionFailureCli.test.ts
|
||||
- name: Native Swift demangling CLI and MCP regressions
|
||||
run: npm run test:focused -- tests/boundary/cli/nativeDemangleCli.test.ts tests/boundary/mcp/nativeDemangleMcp.test.ts
|
||||
- name: Process filesystem snapshot descriptor regressions
|
||||
run: npm run test:focused -- src/process/capture/FilesystemSnapshot.test.ts
|
||||
|
||||
reconstruction-readiness:
|
||||
name: Reconstruction readiness
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
name: Regenerate generated metadata
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "src/**"
|
||||
- "skills/**/SKILL.md"
|
||||
- "scripts/generate-*.mjs"
|
||||
- "scripts/check-doc-facts.mjs"
|
||||
- "scripts/lib/*.mjs"
|
||||
- "scripts/verify/managed/**"
|
||||
- "scripts/fixtures/managed/**"
|
||||
- "package.json"
|
||||
- "package-lock.json"
|
||||
|
||||
concurrency:
|
||||
group: regen-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
HUSKY: "0"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
regen:
|
||||
name: Regenerate and push tracked generated files
|
||||
# Same-repo PRs only: the default token cannot push to forks.
|
||||
# The bot-actor guard stops looped runs triggered by our own push.
|
||||
if: >-
|
||||
github.actor != 'github-actions[bot]' &&
|
||||
github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run docs:generate
|
||||
- name: Commit regenerated files
|
||||
shell: bash
|
||||
env:
|
||||
HEAD_REF: ${{ github.event.pull_request.head.ref }}
|
||||
run: |
|
||||
generated_paths=(
|
||||
docs/product-catalog.json
|
||||
docs/error-contract.schema.json
|
||||
docs/verification/managed-conformance-manifest.json
|
||||
docs/verification/managed-conformance-ledger.json
|
||||
skills/reverse-engineer-anything/SKILL.md
|
||||
src/generatedPackageMetadata.ts
|
||||
)
|
||||
if git diff --quiet -- "${generated_paths[@]}"; then
|
||||
echo "Generated metadata is already canonical."
|
||||
exit 0
|
||||
fi
|
||||
git diff --check
|
||||
# Refuse to sweep unrelated working-tree changes into the bot commit.
|
||||
unexpected="$(git diff --name-only | grep -vxF -f <(printf '%s\n' "${generated_paths[@]}") || true)"
|
||||
if [[ -n "${unexpected}" ]]; then
|
||||
echo "docs:generate left unexpected tracked changes:" >&2
|
||||
printf '%s\n' "${unexpected}" >&2
|
||||
exit 1
|
||||
fi
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add -- "${generated_paths[@]}"
|
||||
git commit -m "chore: regenerate generated metadata"
|
||||
git push origin "HEAD:${HEAD_REF}"
|
||||
@@ -1,15 +1,24 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["release/*"]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_branch:
|
||||
description: Frozen release branch (release/VERSION)
|
||||
required: true
|
||||
type: string
|
||||
phase:
|
||||
description: Prepare the release PR, or publish its reviewed merge
|
||||
required: true
|
||||
default: prepare
|
||||
type: choice
|
||||
options: [prepare, publish]
|
||||
|
||||
env:
|
||||
HUSKY: "0"
|
||||
|
||||
concurrency:
|
||||
group: release-${{ github.ref }}
|
||||
group: release-${{ inputs.release_branch }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
@@ -21,73 +30,111 @@ jobs:
|
||||
pull-requests: write
|
||||
outputs:
|
||||
release_created: ${{ steps.release.outputs.release_created }}
|
||||
tag_name: ${{ steps.release.outputs.tag_name }}
|
||||
sha: ${{ steps.release.outputs.sha }}
|
||||
steps:
|
||||
- name: Validate release selection
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RELEASE_BRANCH: ${{ github.ref_name }}
|
||||
RELEASE_BRANCH: ${{ inputs.release_branch }}
|
||||
RELEASE_PHASE: ${{ inputs.phase }}
|
||||
run: |
|
||||
if [[ "${GITHUB_REF}" != "refs/heads/${RELEASE_BRANCH}" || "${RELEASE_BRANCH}" != release/* ]]; then
|
||||
echo "Select a release/ branch; main and tags are not release checkpoints." >&2
|
||||
if [[ "${RELEASE_PHASE}" != prepare && "${RELEASE_PHASE}" != publish ]]; then
|
||||
echo "phase must be prepare or publish." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "${RELEASE_PHASE}" == prepare && "${GITHUB_REF}" != "refs/heads/main" ]]; then
|
||||
echo "Run prepare from main; release_branch selects the frozen source." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "${RELEASE_PHASE}" == publish && "${GITHUB_REF}" != "refs/heads/${RELEASE_BRANCH}" ]]; then
|
||||
echo "Run publish from the selected frozen release branch so npm provenance records its source." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! "${RELEASE_BRANCH}" =~ ^release/[^/]+$ ]]; then
|
||||
echo "release_branch must select a release/ branch; main is not a release checkpoint." >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! git check-ref-format "refs/heads/${RELEASE_BRANCH}"; then
|
||||
echo "Invalid release branch: ${RELEASE_BRANCH}" >&2
|
||||
echo "Invalid release_branch: ${RELEASE_BRANCH}" >&2
|
||||
exit 1
|
||||
fi
|
||||
selected_sha="$(gh api "repos/${GITHUB_REPOSITORY}/git/ref/heads/${RELEASE_BRANCH}" --jq '.object.sha')"
|
||||
if [[ "${selected_sha}" != "${GITHUB_SHA}" ]]; then
|
||||
echo "The release branch moved after this run was triggered; review its new checkpoint before publication." >&2
|
||||
|
||||
- name: Resolve release branch tip
|
||||
id: branch
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RELEASE_BRANCH: ${{ inputs.release_branch }}
|
||||
run: |
|
||||
sha="$(gh api "repos/${GITHUB_REPOSITORY}/git/ref/heads/${RELEASE_BRANCH}" --jq '.object | select(.type == "commit") | .sha')"
|
||||
if [[ ! "${sha}" =~ ^[0-9a-f]{40}$ ]]; then
|
||||
echo "The release branch tip is not a commit." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "sha=${sha}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Require the publish dispatch to match the branch tip
|
||||
if: inputs.phase == 'publish'
|
||||
shell: bash
|
||||
env:
|
||||
BRANCH_SHA: ${{ steps.branch.outputs.sha }}
|
||||
run: |
|
||||
if [[ "${BRANCH_SHA}" != "${GITHUB_SHA}" ]]; then
|
||||
echo "The release branch tip differs from the dispatched checkpoint; refusing to create a release for a moved source." >&2
|
||||
exit 1
|
||||
fi
|
||||
printf 'Selected release checkpoint: %s\n' "${selected_sha}"
|
||||
|
||||
- uses: googleapis/release-please-action@v4
|
||||
id: release
|
||||
with:
|
||||
# A dedicated credential lets release-PR updates trigger CI. GitHub
|
||||
# requires manual approval for pull_request events from GITHUB_TOKEN.
|
||||
# A dedicated credential starts PR CI automatically; the built-in
|
||||
# token creates approval-required PR runs for maintainers to approve.
|
||||
token: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
target-branch: ${{ github.ref_name }}
|
||||
target-branch: ${{ inputs.release_branch }}
|
||||
skip-github-release: ${{ inputs.phase == 'prepare' }}
|
||||
skip-github-pull-request: ${{ inputs.phase == 'publish' }}
|
||||
|
||||
- name: Bind publication to the triggering checkpoint
|
||||
if: steps.release.outputs.release_created == 'true'
|
||||
- name: Require a merged release PR for publication
|
||||
if: inputs.phase == 'publish' && steps.release.outputs.release_created != 'true'
|
||||
run: |
|
||||
echo "No new release was created. Merge the reviewed release PR into its frozen branch before publishing. For a partial publication, re-run failed jobs in the original run." >&2
|
||||
exit 1
|
||||
|
||||
- name: Bind publication to the dispatched checkpoint
|
||||
if: inputs.phase == 'publish' && steps.release.outputs.release_created == 'true'
|
||||
shell: bash
|
||||
env:
|
||||
RELEASE_SHA: ${{ steps.release.outputs.sha }}
|
||||
run: |
|
||||
if [[ "${RELEASE_SHA}" != "${GITHUB_SHA}" ]]; then
|
||||
echo "The tagged release SHA differs from the triggering checkpoint; refusing to publish mismatched source provenance." >&2
|
||||
echo "The tagged release SHA differs from the dispatched checkpoint; refusing to publish mismatched source provenance. The GitHub release already exists; do not dispatch publish again." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Check out release pull request
|
||||
if: steps.release.outputs.prs_created == 'true'
|
||||
if: inputs.phase == 'prepare' && steps.release.outputs.prs_created == 'true'
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ fromJSON(steps.release.outputs.pr).headBranchName }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node.js for generated documentation
|
||||
if: steps.release.outputs.prs_created == 'true'
|
||||
if: inputs.phase == 'prepare' && steps.release.outputs.prs_created == 'true'
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
package-manager-cache: false
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.release.outputs.prs_created == 'true'
|
||||
if: inputs.phase == 'prepare' && steps.release.outputs.prs_created == 'true'
|
||||
run: npm ci
|
||||
|
||||
- name: Regenerate release documentation
|
||||
if: steps.release.outputs.prs_created == 'true'
|
||||
if: inputs.phase == 'prepare' && steps.release.outputs.prs_created == 'true'
|
||||
run: npm run docs:generate
|
||||
|
||||
- name: Commit canonical release catalog
|
||||
if: steps.release.outputs.prs_created == 'true'
|
||||
if: inputs.phase == 'prepare' && steps.release.outputs.prs_created == 'true'
|
||||
shell: bash
|
||||
env:
|
||||
# Use the same identity for the follow-up commit so it also triggers CI.
|
||||
@@ -127,7 +174,7 @@ jobs:
|
||||
publish:
|
||||
name: Publish to npm
|
||||
needs: release-please
|
||||
if: needs.release-please.outputs.release_created == 'true'
|
||||
if: inputs.phase == 'publish' && needs.release-please.outputs.release_created == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -173,28 +220,32 @@ jobs:
|
||||
verification_root="$(mktemp -d)"
|
||||
trap 'rm -rf -- "${verification_root}"' EXIT
|
||||
cd "${verification_root}"
|
||||
for attempt in $(seq 1 80); do
|
||||
max_attempts=120
|
||||
sleep_seconds=15
|
||||
total_window_minutes=$((max_attempts * sleep_seconds / 60))
|
||||
for attempt in $(seq 1 "${max_attempts}"); do
|
||||
published_version="$(npm view --prefer-online "rea-agents@${version}" version --json 2>/dev/null || true)"
|
||||
if [ "${published_version}" = "\"${version}\"" ]; then
|
||||
echo "rea-agents@${version} is visible on npm after attempt ${attempt}; verifying the published CLI."
|
||||
echo "rea-agents@${version} is visible on npm after attempt ${attempt}/${max_attempts}; verifying the published CLI."
|
||||
npm exec --yes --package="rea-agents@${version}" -- rea --help
|
||||
npm exec --yes --package="rea-agents@${version}" -- rea --llms
|
||||
node "${GITHUB_WORKSPACE}/scripts/verify-published-package.mjs" "${version}"
|
||||
exit 0
|
||||
else
|
||||
echo "rea-agents@${version} is not visible on npm yet (attempt ${attempt}/80; up to 20 minutes)" >&2
|
||||
echo "rea-agents@${version} is not visible on npm yet (attempt ${attempt}/${max_attempts}; up to ${total_window_minutes} minutes)" >&2
|
||||
fi
|
||||
if [ "${attempt}" -lt 80 ]; then
|
||||
sleep 15
|
||||
if [ "${attempt}" -lt "${max_attempts}" ]; then
|
||||
sleep "${sleep_seconds}"
|
||||
fi
|
||||
done
|
||||
echo "rea-agents@${version} did not become visible on npm within 20 minutes" >&2
|
||||
echo "rea-agents@${version} did not become visible on npm within ${total_window_minutes} minutes (${max_attempts} attempts × ${sleep_seconds}s)" >&2
|
||||
echo "This indicates npm registry propagation delay, not a CLI verification failure." >&2
|
||||
exit 1
|
||||
|
||||
publish-mcp:
|
||||
name: Publish to MCP Registry
|
||||
needs: [release-please, publish]
|
||||
if: needs.release-please.outputs.release_created == 'true' && needs.publish.result == 'success'
|
||||
if: inputs.phase == 'publish' && needs.release-please.outputs.release_created == 'true' && needs.publish.result == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -2,7 +2,7 @@ name: Website checks
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
branches: [main, "release/*"]
|
||||
paths:
|
||||
- "website/**"
|
||||
- "scripts/verify-website.py"
|
||||
|
||||
@@ -48,3 +48,6 @@ Thumbs.db
|
||||
|
||||
# Generated website download
|
||||
/website/public/examples/notes-example.zip
|
||||
|
||||
# Build-generated MCP tool catalog (regenerated by `npm run build`; never commit)
|
||||
src/generatedMcpToolCatalog.ts
|
||||
|
||||
@@ -25,6 +25,7 @@ REA is a layered ESM TypeScript application. Dependencies flow inward from pure
|
||||
- `src/process/` owns shared process lifecycle primitives, not provider wire protocols. `bridge/` contains provider-side adapters.
|
||||
- `tests/` contains unit, composition, boundary, acceptance, and conformance tests. `scripts/verify-*` and capability directories under `scripts/verify/` contain real-toolchain checks.
|
||||
- `docs/product-catalog.json` is generated. Update its source contracts and regenerate it; do not edit it directly.
|
||||
- `src/generatedMcpToolCatalog.ts` is build-generated and gitignored. Never commit it; resolve any trace of it in merges by deleting it and running `npm run build:cached`.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
|
||||
|
||||
+12
-10
@@ -28,19 +28,21 @@ Keep dependencies flowing inward through the existing domain, contracts, provide
|
||||
|
||||
## Documentation website
|
||||
|
||||
The VitePress site uses the Markdown files in `docs/` and deploys to
|
||||
<https://morluto.github.io/rea/>. Run `npm run docs:dev` for live editing,
|
||||
`npm run docs:build` to check the production build and links, and
|
||||
`npm run docs:preview` to preview that build at `/rea/`.
|
||||
The VitePress site uses the Markdown files in `docs/`. Run `npm run docs:dev`
|
||||
for live editing, `npm run docs:build` to check the production build and
|
||||
links, and `npm run docs:preview` to preview that build at `/rea/`.
|
||||
|
||||
Site navigation lives in `docs/.vitepress/config.ts`. Keep links to guides
|
||||
relative so they work on GitHub and the website; link to repository files
|
||||
outside `docs/` using their full GitHub URLs. Generated reference documents
|
||||
still use `npm run docs:generate`; `docs:build` only builds the website.
|
||||
|
||||
The documentation workflow builds pull requests and deploys changes on `main`.
|
||||
The repository's **Settings → Pages → Build and deployment → Source** must be
|
||||
set to **GitHub Actions** before the first deployment.
|
||||
Pull requests run `npm run docs:check`. `.github/workflows/pages.yml` is a
|
||||
manual VitePress build and does not publish. The public site at
|
||||
<https://morluto.github.io/rea/> is published only by the manual website
|
||||
workflow on `main`. The repository's **Settings → Pages → Build and
|
||||
deployment → Source** must be set to **GitHub Actions** before the first
|
||||
deployment.
|
||||
|
||||
## Development feedback and PR verification
|
||||
|
||||
@@ -121,9 +123,9 @@ Describe the behavior change and verification performed in the pull request. Nev
|
||||
## Maintainer release checklist
|
||||
|
||||
Use the [checkpoint release guide](docs/releasing.md). Releases start from an
|
||||
explicit `release/VERSION` branch cut at a recorded commit. Pushing the branch prepares
|
||||
the bot PR. Wait for its exact-head CI and review; merging into that frozen
|
||||
branch triggers publication through the official Release workflow. Main
|
||||
explicit `release/VERSION` branch cut at a recorded commit. Manually prepare
|
||||
the bot PR, wait for its exact-head CI and review, merge into that frozen
|
||||
branch, and manually publish through the official Release workflow. Main
|
||||
pushes do not update release PRs or publish packages. Both publishers build
|
||||
the exact SHA tagged by Release Please.
|
||||
|
||||
|
||||
+137
-85
@@ -4,9 +4,9 @@
|
||||
|
||||
# REA:あらゆるものをリバースエンジニアリング
|
||||
|
||||
### エージェントでアプリの動作からネイティブバイナリまで調べる。
|
||||
### バイナリ、アプリ、実行時の動作をひとつの MCP でリバースエンジニアリング。
|
||||
|
||||
**気になる機能を見つけ、仕組みを理解し、自分の形で実装する。**
|
||||
**気に入った機能を見つけたら、その仕組みをバイナリのレベルまで理解する。**
|
||||
|
||||
[](https://www.npmjs.com/package/rea-agents)
|
||||
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
||||
@@ -45,11 +45,11 @@
|
||||
|
||||
---
|
||||
|
||||
アプリの機能を自分のプロダクトにも取り入れたいときは、REA を使ってエージェントに調査を依頼できます。ソースコードがなくても、アプリを調べ、仕組みと根拠を示し、あなたのプロジェクト向けに同様の機能を実装できます。
|
||||
自分のプロダクトにも欲しい機能を他のアプリで見つけたら、REA を使ってエージェントに調査を依頼できます。エージェントはソースコードがなくてもアプリを調べ、機能の仕組みを説明して根拠を示し、あなたのプロジェクト向けの実装まで進められます。
|
||||
|
||||
REA はネイティブバイナリ、JavaScript/Electron アプリ、.NET アセンブリ、Web サイトの解析ツールを提供します。エージェントからもターミナルからも使えます。解析はローカルで実行され、結果には根拠と制限が含まれます。
|
||||
REA は、ネイティブバイナリ、JavaScript/Electron アプリ、.NET アセンブリ、Web サイトを調べるためのツールをエージェントに提供します。同じツールはターミナルからも使えます。解析はローカルで実行され、結果には各結論の根拠と制限が含まれます。
|
||||
|
||||
Setup はエージェントを設定し、既存の Hopper または Ghidra に接続します。承認後に Hopper をインストールすることもできます。
|
||||
セットアップは REA をエージェントに登録し、対応するワークフロー手順もインストールします。ネイティブ解析には既存の Hopper または Ghidra を使えます。承認すれば、セットアップで Hopper をインストールすることもできます。静的な JavaScript 解析にはどちらのエンジンも不要です。
|
||||
|
||||
## エージェントに頼むだけ
|
||||
|
||||
@@ -59,7 +59,7 @@ Setup はエージェントを設定し、既存の Hopper または Ghidra に
|
||||
メモアプリの検索機能を調べて根拠を示し、私のプロジェクト向けに同様の機能を実装してください。
|
||||
```
|
||||
|
||||
メモアプリを調べたいアプリに置き換えるか、まず概要を依頼してください。
|
||||
「メモアプリ」を調べたいアプリに置き換えてください。まずは概要の調査を依頼することもできます。
|
||||
|
||||
## バイナリから動作へ
|
||||
|
||||
@@ -67,56 +67,73 @@ Setup はエージェントを設定し、既存の Hopper または Ghidra に
|
||||
| -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| ネイティブアプリや実行ファイルから、プロシージャ、疑似コード、アセンブリ、文字列、シンボル、セグメント、メタデータを復元します。 | 呼び出し元、呼び出し先、クロスリファレンス、コールグラフをたどり、機能やアルゴリズムの実際の動作を説明できる状態にします。 | エージェントが得た知見を、あなたの技術スタック、画面、要件に合うプロダクト機能へ変えます。 |
|
||||
|
||||
REA は調査をバイナリ上の根拠に結び付けます。元のソースコードを復元したり、アプリ全体を自動複製したりするとは主張しません。
|
||||
REA は結論に至った根拠を示します。元のソースコードを復元できる、あるいはアプリ全体を自動的に複製できるとは主張しません。
|
||||
|
||||
## REA を選ぶ理由
|
||||
|
||||
| | |
|
||||
| -------------------------- | ---------------------------------------------------------------------------------------------------- |
|
||||
| **エージェント向け** | アプリの動作について質問し、推測ではなくエージェントに調査させることができます。 |
|
||||
| **CLI と MCP** | ターミナルとエージェントから同じリバースエンジニアリング機能を使えます。 |
|
||||
| **ガイド付きセットアップ** | エージェントの設定、既存の解析ツールへの接続、承認後の Hopper インストールに対応します。 |
|
||||
| **洞察からコードへ** | 機能を理解したら、同じコーディングセッションで自分のプロダクト向けに実装できます。 |
|
||||
| **ローカルで解析** | 解析は対応するローカルホストで実行され、REA がバイナリをホスト型解析サービスへ送ることはありません。 |
|
||||
| **コンテキストを維持** | 質問ごとに解析を最初からやり直さず、複数のアプリを続けて調査できます。 |
|
||||
| | |
|
||||
| -------------------------- | -------------------------------------------------------------------------------------------------------- |
|
||||
| **エージェント向け** | アプリの動作を質問すると、エージェントが推測ではなく実際に調べて答えます。 |
|
||||
| **CLI と MCP** | 同じリバースエンジニアリング機能を、ターミナルからもエージェントからも使えます。 |
|
||||
| **ガイド付きセットアップ** | エージェントの設定、既存の解析ツールへの接続、承認したうえでの Hopper のインストールに対応します。 |
|
||||
| **洞察からコードへ** | 機能を理解したら、同じコーディングセッションで自分のプロダクト向けに実装できます。 |
|
||||
| **ローカルで解析** | 解析は対応するローカルホストで実行されます。REA がアプリをホスト型の解析サービスへ送ることはありません。 |
|
||||
| **コンテキストを維持** | 質問のたびに解析をやり直すことなく、複数のアプリを続けて調査できます。 |
|
||||
|
||||
## クイックスタート
|
||||
|
||||
### セットアップを実行(推奨)
|
||||
|
||||
REA をエージェントで使うための設定を行います:
|
||||
REA をエージェントで使えるように設定します:
|
||||
|
||||
```bash
|
||||
npx rea-agents setup
|
||||
```
|
||||
|
||||
Setup は最初に連携するエージェントを複数選択できるようにします。既存の REA 登録は初期選択されますが、検出されただけのクライアントは自動選択されず、未設定のクライアントも選べます。具体的なパスと変更内容を確認してから承認してください。選択したエージェントには通常 REA のワークフローをインストールします。Hopper は別の任意操作で、個別の承認が必要です。既存の Ghidra のパスも登録できます。
|
||||
セットアップでは、まず REA を使うエージェントを選び(複数選択可)、具体的なパスと変更内容を確認してから承認します。既存の REA 登録は最初から選択されています。新しく検出されたエージェントは選択肢に表示されますが、検出されただけでは選択されません。選択したエージェントには、MCP 接続と REA のガイド付きワークフローが追加されます。Hopper のインストールは別の任意項目で、個別に承認が必要です。既存の Ghidra のインストール先を登録することもできます。
|
||||
|
||||
変更は事前に表示され、既存の設定はバックアップされます。要件と追加オプションは[インストールガイド](docs/installation.md)を参照してください。
|
||||
セットアップは変更を適用する前に内容を表示し、既存の設定をバックアップします。要件とセットアップのオプションは[インストールガイド](docs/installation.md)を参照してください。
|
||||
|
||||
### エージェントで使う
|
||||
### エージェントで使う(推奨)
|
||||
|
||||
設定後にエージェントを再起動し、調べたいアプリや機能を説明します。REA は Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf、Devin、OpenCode、Antigravity、GitHub Copilot CLI、Command Code、VS Code に対応します。既存の REA 登録は初期選択され、それ以外の検出済みクライアントは選択が必要です。その他のエージェントは下記の MCP 設定を使えます。
|
||||
セットアップ後にエージェントを再起動し、理解したいアプリや機能を[説明してください](#エージェントに頼むだけ)。Hopper はデモモードでも動作します。初回起動の画面が表示されたら、デモを選ぶか既存のライセンスを入力してください。
|
||||
|
||||
Hopper はデモモードで使えます。初回起動の画面ではデモを選択するか、既存のライセンスを入力してください。
|
||||
REA は Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf、Devin、OpenCode、Antigravity、GitHub Copilot CLI、Command Code、VS Code に対応しています。セットアップでは既存の REA 登録が最初から選択され、それ以外の検出済みエージェントは選択するまで対象になりません。その他のエージェントは[手動の MCP 設定](#他のコーディングエージェントで使う)を使えます。
|
||||
|
||||
### AI コーディングアシスタント向けスキル(任意)
|
||||
|
||||
AI コーディングアシスタントにスキルを追加すると、より豊富なコンテキストが得られます:
|
||||
AI コーディングアシスタントにスキルを追加すると、より豊富なコンテキストを利用できます:
|
||||
|
||||
```bash
|
||||
npx skills add morluto/rea --skill reverse-engineer-anything
|
||||
```
|
||||
|
||||
このスキルは REA の調査ワークフローを提供します。上記の setup を実行して REA をエージェントに接続し、解析ツールを設定してください。Setup は既定でバージョンに合ったスキルをインストールします。このコマンドではリポジトリ版のスキルをインストールします。
|
||||
このスキルは REA の調査ワークフローを提供します。REA をエージェントに接続して解析ツールを設定するには、上記のセットアップを実行してください。セットアップは既定でバージョンの一致したスキルをインストールします。このコマンドはリポジトリ版のスキルをインストールします。
|
||||
|
||||
展開済みの JavaScript/Electron アプリのツリーや ASAR であれば、MCP の設定もネイティブエンジンも不要です:
|
||||
### ターミナルで最初の結果を得る
|
||||
|
||||
展開済みの JavaScript/Electron アプリのディレクトリまたは ASAR に対して、次を実行します:
|
||||
|
||||
```bash
|
||||
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
|
||||
```
|
||||
|
||||
パスを調査対象に置き換えてください(Windows では `"D:/apps/example"` のようなパス)。この静的ワークフローではそのようなパスも扱えます。ネイティブ解析には専用のエンジン設定が必要です。
|
||||
パスは調査対象に置き換えてください(Windows では `"D:/apps/example"` など)。MCP の設定、Hopper、Ghidra を必要とせず、アプリを実行することもなく、Evidence、復元したグラフ、制限、未解決事項をその場で返します。ネイティブアプリの場合は、先にエンジンを設定してから、そのアプリのパスを指定して `analyze` を使います。`doctor` は問題を診断したいときに使うもので、解析のたびに実行する必要はありません。
|
||||
|
||||
### 作業に合わせて準備状況を確認する
|
||||
|
||||
オプションなしの `rea doctor` は連携全体の監査です。検出したすべてのエージェント登録、インストール済みのスキル、すべての任意の解析エンジンを確認するため、今の作業には支障がなくても `healthy: false` を報告することがあります。作業に合わせて確認範囲を指定してください:
|
||||
|
||||
| 作業 | 準備状況の確認 |
|
||||
| ------------------------------------------ | ---------------------------------------------------------------------------------------------- |
|
||||
| 静的な JavaScript/Electron 解析 | 不要です。`analyze-javascript-application` を直接実行します。 |
|
||||
| 特定の解析エンジンのトラブルシューティング | `rea doctor --provider ghidra --json`(または `hopper`、`ida`) |
|
||||
| 特定のエージェントの MCP 登録を確認 | `rea doctor --client codex --json`([クライアント ID](docs/installation.md#supported-agents)) |
|
||||
| インストール済みのスキルを確認 | `rea doctor --skill --json` |
|
||||
|
||||
範囲を指定したレポートでは、`healthy` と終了ステータスを決めるのは `scope_checks` だけです。それ以外の項目は `informational_checks` に表示され、今の作業のために修正する必要はありません。たとえば、使っていないエンジンが見つからなくても対処は不要です。
|
||||
|
||||
ネイティブ対象に対応するエンジンが複数インストールされている場合、REA は自動ではどれも選びません。対象を開くと `code: "capability_unavailable"` と `details.selection_reason: "ambiguous"` で失敗し、`details.candidate_ids` に候補が示されます。CLI の `--provider` か `open_binary` の `provider_id` で一度選ぶか、常用する設定として `REA_ANALYSIS_PROVIDER` を指定してください。詳しくは[英語版の解説](README.md#choosing-a-deep-analysis-provider)を参照してください。
|
||||
|
||||
### rea コマンドをインストール
|
||||
|
||||
@@ -126,9 +143,9 @@ npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --
|
||||
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
|
||||
```
|
||||
|
||||
Node.js と npm を先にインストールしてください。ターミナルで実行すると、インストーラーは `rea` を追加してセットアップを開始します。
|
||||
Node.js と npm を先にインストールしておいてください。ターミナルで実行すると、インストーラーは `rea` を追加してセットアップを開始します。
|
||||
|
||||
npm でインストールし、セットアップを実行することもできます:
|
||||
npm でインストールしてから、セットアップを実行することもできます:
|
||||
|
||||
```bash
|
||||
npm install --global rea-agents
|
||||
@@ -137,38 +154,56 @@ rea setup
|
||||
|
||||
### 要件
|
||||
|
||||
静的な JavaScript 解析に必要なのは Node.js と npm だけです。ホストと外部ツールの前提条件は使うワークフローによって異なり、ネイティブ解析プロバイダーの対応プラットフォームは各ガイドで説明しています。
|
||||
|
||||
- macOS 12 以降
|
||||
- Ubuntu 24.04+、Fedora 41+、64 ビット Arch Linux、または CachyOS
|
||||
- Node.js 22.x (>=22.19)、24.x (>=24.11)、または 26+ と npm
|
||||
- Node.js 22.x (>=22.19)、24.x (>=24.11)、または 26+
|
||||
- npm(特定のバージョンは不要で、REA が npm をインストールすることもありません)
|
||||
|
||||
ネイティブバイナリ解析には Hopper または Ghidra が必要です。Hopper は別製品です。デモにはベンダー所定の制限がありますが、有料ライセンスは必須ではありません。
|
||||
ネイティブバイナリの詳細な解析には Hopper、Ghidra、IDA Pro のいずれかが必要です。Hopper は独自のライセンスを持つ別製品で、デモ版でもベンダーが定める制限の範囲で解析できます。Ghidra と IDA は、利用者が用意したものを使います。
|
||||
|
||||
Ghidra は Linux x64 と macOS x64/arm64 に対応します。Ghidra 12.1.x と、そのインストールが宣言する完全な 64 ビット JDK(`application.java.min` から `application.java.max` まで)を別途インストールし、REA で使うように設定してください。現行の 12.1 リリースは JDK 21 以降を要求し、上限は設定しません。ブリッジは Ghidra 12.1.4 と JDK 21 で検証しています。macOS ではホストのアーキテクチャに合うネイティブデコンパイラーも必要です。
|
||||
Ghidra は Linux x64 と macOS x64/arm64 に対応しています。Ghidra 12.1.x と、そのインストールが宣言する 64 ビットの完全な JDK(`application.java.min` から `application.java.max` まで)を別途インストールし、REA で使うように設定してください。現行の 12.1 リリースは JDK 21 以降を必要とし、上限はありません。ブリッジは Ghidra 12.1.4 と JDK 21 で検証しています。macOS では、ホストのアーキテクチャに合うネイティブデコンパイラーも必要です。
|
||||
|
||||
Setup はインストールを確認し、パスを保存できます。Ghidra、Java、Node.js、npm、Homebrew のインストールや更新は行いません。
|
||||
```bash
|
||||
export GHIDRA_INSTALL_DIR=/absolute/path/to/ghidra_12.1.4_PUBLIC
|
||||
export JAVA_HOME=/absolute/path/to/jdk-21 # java と javac が PATH から解決できる場合は省略可
|
||||
rea doctor --provider ghidra --json
|
||||
rea setup
|
||||
```
|
||||
|
||||
リポジトリの main と npm 4.1.0 には、ローカル NTFS 上のネイティブ x86-64 PE アプリケーション(非マネージド、非 DLL 向け)を対象とする実験的な Windows x64 Ghidra P0 対応が含まれています。Job Object、プライベート DACL、パス許可の各制御が同梱されています。古い npm パッケージに期待する前に[リリース境界](docs/installation.md#released-package-and-main)を確認してください。前提条件と検証済みの範囲は [Windows Ghidra P0](docs/windows-ghidra-p0.md) を参照してください。
|
||||
セットアップはこれらのインストールを確認し、承認後にパスを保存します。Ghidra、Java、Node.js、npm、Homebrew のインストールや更新は行いません。
|
||||
|
||||
IDA Pro は、すでに動作している [mrexodia/ida-pro-mcp](https://github.com/mrexodia/ida-pro-mcp) の MCP 登録を再利用します。開いている GUI の対象を読み取り専用で解析するか(`attached`、既定)、データベースのスーパーバイザーで指定したバイナリをヘッドレスで開いて解析します(`headless`)。REA が attached の GUI データベースを保存したり閉じたりすることはありません。最初の実環境での検証は、Windows の GUI と Windows x64 のヘッドレス IDA 9.3 が対象です。設定方法と対応範囲は [IDA プロバイダーガイド](docs/ida-provider.md)を参照してください。
|
||||
|
||||
```bash
|
||||
export REA_IDA_MCP_CONFIG=/absolute/path/to/ida-mcp.json
|
||||
rea function /absolute/path/to/program main --provider ida --json
|
||||
```
|
||||
|
||||
リポジトリの main と npm 4.1.0 には、ローカル NTFS 上のネイティブ x86-64 PE アプリケーション(マネージドコードと DLL を除く)を対象とする、実験的な Windows x64 Ghidra P0 対応が含まれています。Job Object、プライベート DACL、パスの受け入れ検査の各制御も同梱されています。古い npm パッケージでこの機能を使えるか確認するには[リリース境界](docs/installation.md#released-package-and-main)を、前提条件と検証済みの範囲は [Windows Ghidra P0](docs/windows-ghidra-p0.md) を参照してください。
|
||||
|
||||
### トラブルシューティング
|
||||
|
||||
`npx -y rea-agents@latest doctor` はホスト、依存関係、解析ツール、エージェント設定を変更せずに確認します。構造化された診断には `--json` を追加してください。
|
||||
`npx -y rea-agents@latest doctor` は、ホスト、依存関係、解析ツール、エージェント設定を変更せずに確認します。構造化された診断結果が必要な場合は `--json` を追加してください。
|
||||
|
||||
Linux では実行可能な `/opt/hopper/bin/Hopper` を優先し、利用できない場合は `~/.local/share/rea/hopper/bin/Hopper` を自動的に確認します。それ以外の場所では `HOPPER_LAUNCHER_PATH` を設定します。ファイルがあるのに解析エンジンが見つからない場合は、実際の Hopper パスに対して `ldd /absolute/path/to/Hopper | grep 'not found'` を実行し、不足ライブラリを確認してください。詳しくは [Hopper ガイド](docs/installation.md#hopper)を参照してください。
|
||||
Linux では実行可能な `/opt/hopper/bin/Hopper` を優先し、利用できない場合は `~/.local/share/rea/hopper/bin/Hopper` を自動的に確認します。それ以外の場所にインストールした場合は `HOPPER_LAUNCHER_PATH` を設定してください。ファイルがあるのに解析エンジンが見つからないと報告される場合は、実際の Hopper のパスに対して `ldd /absolute/path/to/Hopper | grep 'not found'` を実行し、不足しているライブラリを確認してください。詳しくは [Hopper ガイド](docs/installation.md#hopper)を参照してください。
|
||||
|
||||
### 更新とアンインストール
|
||||
|
||||
- `rea update` は現在の REA インストールを更新します。
|
||||
- `rea uninstall` は REA が管理するエージェント登録とワークフローファイルを削除します。Hopper は残ります。
|
||||
- `rea uninstall --purge-data` は REA のキャッシュと状態も削除します。それらを削除したい場合にだけ使ってください。
|
||||
- `rea uninstall` は REA が管理するエージェント登録とスキルだけを削除します。Hopper、Node.js、Evidence ファイル、キャプチャ、無関係なスキル、他の MCP サーバーは残ります。
|
||||
- `rea uninstall --purge-data` は `~/.rea/cache` と `~/.rea/state` も削除します。それらを消したい場合にだけ使ってください。
|
||||
|
||||
## 現在の対応状況
|
||||
|
||||
リポジトリの現在の機能とプラットフォーム要件は[英語の対応状況ガイド](README.md#current-status)で説明しています。main は [npm リリース](docs/installation.md#released-package-and-main)より先行する場合があります。
|
||||
|
||||
- Ghidra は Linux x64、macOS x64/arm64、実験的な Windows x64 P0 境界で 25 件の読み取り専用操作を提供します。Linux/macOS ではアトミックなセッション内関数注釈にも対応します。Windows P0 は読み取り専用であり、Ghidra に GUI 操作はありません。
|
||||
- 静的な Android 検査には持ち込みの JADX/Java が別途必要です。実プロバイダー検証は Linux を対象とします。詳しくは[Android 解析](docs/android-analysis.md)を参照してください。
|
||||
- ブラウザー、Electron、プロセスの各リクエストは対象・操作・ライフサイクルを直接指定し、ホストの実際のアクセスに依存します。REA 側の個別の許可付与は不要です。Setup による設定書き込みと Hopper のインストールは、引き続き正確な計画の承認が必要です。
|
||||
- `rea capabilities` はバイナリセッション操作の説明であり、すべてのアプリ/ランタイムツールの目録ではありません。接続済み MCP ツール一覧と関連ガイドを参照してください。
|
||||
- Ghidra は Linux x64、macOS x64/arm64、実験的な Windows x64 P0 境界で 25 種類の読み取り専用操作を提供します。Linux と macOS では、セッション内での関数名とエントリーコメントのアトミックな編集にも対応します。Windows P0 には変更の権限がなく、Ghidra には REA から操作できる GUI 機能はありません。
|
||||
- IDA アダプターは、ドキュメントに記載された読み取り専用の関数・文字列操作を提供します。
|
||||
- 静的な Android APK の検査は Linux と macOS に対応し、別途用意したヘッドレス JADX と完全な JDK が必要です。現在のメタデータブリッジは macOS arm64 で検証しています。詳しくは [Android 解析](docs/android-analysis.md)を参照してください。
|
||||
- ブラウザー、Electron、プロセスの各リクエストは対象、操作、ライフサイクルを直接指定し、ホストで実際に許可されたアクセスの範囲で動作します。REA 側で別途権限を付与する必要はありません。セットアップによる設定の書き込みと Hopper のインストールには、引き続き具体的な計画の承認が必要です。
|
||||
- `rea capabilities` と `rea providers` はバイナリセッションのプロバイダーと補助操作を説明するもので、ブラウザー、Android、アプリのすべてのワークフローの一覧ではありません。MCP の機能全体は、接続中の MCP ツール一覧と `binary_session` が示すツールの利用可否で確認してください。
|
||||
|
||||
## ひとつのプロンプトで調査を完結
|
||||
|
||||
@@ -181,49 +216,54 @@ TypeScript と SQLite を使って私のプロジェクト向けに実装して
|
||||
| ---: | ------------------------------ | ---------------------------------------------------------------- |
|
||||
| 1 | バイナリを開いて識別 | `open_binary`, `binary_overview` |
|
||||
| 2 | オフライン検索の手掛かりを探す | `search_strings`, `search_procedures`, `list_names` |
|
||||
| 3 | 手掛かりと実行コードを接続 | `find_xrefs_to_name`, `xrefs`, `procedure_callers` |
|
||||
| 4 | 制御フローを復元 | `get_call_graph`, `procedure_callees`, `procedure_info` |
|
||||
| 3 | 手掛かりを実行コードに結び付け | `find_xrefs_to_name`, `xrefs`, `procedure_callers` |
|
||||
| 4 | 関連する制御フローを復元 | `get_call_graph`, `procedure_callees`, `procedure_info` |
|
||||
| 5 | 関連する処理を逆コンパイル | `procedure_pseudo_code`, `procedure_assembly`, `batch_decompile` |
|
||||
| 6 | プロジェクトに機能を実装する | 技術スタック、プロダクト、要件に合わせたコード |
|
||||
| 6 | プロジェクトに機能を実装 | 技術スタック、プロダクト、要件に合わせたコード |
|
||||
|
||||
REA は手順 1〜5 のバイナリ解析を処理し、手順 6 はエージェントの通常の編集・テストツールが行います。
|
||||
手順 1〜5 のアプリ解析は REA が担当します。手順 6 は、アプリについて得た知見をもとに、エージェントが通常の編集ツールとテストツールで行います。
|
||||
|
||||
## エージェントにできること
|
||||
|
||||
- ソースコードがない機能の仕組みを説明する。
|
||||
- アプリの認証、保存、更新、ネットワークフローを復元する。
|
||||
- 非公開の形式やインターフェースを文書化できる構造を回収する。
|
||||
- 文字列やシンボルから疑わしい動作の実装コードまで追跡する。
|
||||
- 同じセッションで 2 バージョンを切り替え、実装経路を比較する。
|
||||
- 気になる機能を調査し、自分のプロダクトに合わせた形で実装する。
|
||||
- 復元した動作をプロダクト機能、テスト、移行ノート、移植、相互運用できる代替実装へ変換する。
|
||||
- Swift / Objective-C のメタデータを解析する。
|
||||
- Hopper に名前、コメント、ブックマークを残し、人間とエージェントの調査を共有する。
|
||||
- 気に入った機能を調査し、自分のプロダクトに合わせた形で実装する。
|
||||
- ソースコードが手に入らない機能の仕組みを説明する。
|
||||
- アプリの認証、保存、更新、ネットワーク処理の流れを復元する。
|
||||
- 文書化されていない形式やインターフェースについて、文書化に必要な構造を復元する。
|
||||
- 文字列やシンボルから、疑わしい動作を実装しているコードまでたどる。
|
||||
- 復元した動作を、プロダクト機能、テスト、移行メモ、移植版、相互運用できる代替実装に変える。
|
||||
- マングルされたシンボルをひとつずつ手作業で読み解かなくても、Swift と Objective-C のメタデータを解析する。
|
||||
- Hopper に名前、コメント、ブックマークを残し、人とエージェントの解析を互いに活かす。
|
||||
|
||||
## 調査ツールカタログ
|
||||
|
||||
| ツール分類 | 数 | 用途 |
|
||||
| -------------------- | --: | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| ネイティブ検査 | 41 | 関数、疑似コード、アセンブリ、文字列、シンボル、呼び出し、参照、注釈、バイト読み取り、ファイルオフセット |
|
||||
| 調査ワークフロー | 14 | アプリ概要、関数ドシエ、ネイティブ API とディスパッチ、一括逆コンパイル、機能トレース、コールパス、コールグラフ、Swift と Objective-C の検出 |
|
||||
| macOS ネイティブ | 7 | Hopper を起動せずに扱える Mach-O メタデータ、署名、plist、アーキテクチャ、Swift 名の復元 |
|
||||
| 成果物グラフ | 5 | ディレクトリとパッケージの目録、コンパイル済み Interface Builder ファイル、Apple アセットカタログ、抽出 |
|
||||
| マネージド PE/CLI | 7 | .NET の識別、メタデータ、CIL 命令、ネイティブ依存関係、再構築インポート、ビルド比較 |
|
||||
| ファームウェア | 2 | Linux ファームウェア領域の検査と明示的な展開 |
|
||||
| Android APK | 5 | パッケージと manifest の宣言、クラス検索、メンバー一覧、メソッド逆コンパイル、静的参照 |
|
||||
| ブラウザー観察 | 11 | ページ構成、ネットワークメタデータ、スクリプト、ソースマップ、WebMCP 検出、スクリーンショット、キャプチャ比較 |
|
||||
| Electron 解析 | 5 | レンダラー観察、静的アプリマッピング、静的/実行時結果の対応付け |
|
||||
| JavaScript 実行時 | 2 | Node/Electron Inspector 対象の検出、スクリプト位置、実行コンテキストイベント |
|
||||
| アプリワークフロー | 13 | 取得した Web スクリプトのエクスポート、層をまたぐ機能トレース、ビルド比較、履歴ソース対応、静的戻り値形状の比較、再実装検証 |
|
||||
| ワークスペースと観察 | 21 | セッション、証跡バンドル、ナビゲーション文脈、プロセス・成果物・関数比較、未解決事項の記録 |
|
||||
| ツール分類 | 数 | 用途 |
|
||||
| -------------------- | --: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| ネイティブ検査 | 41 | 関数、疑似コード、アセンブリ、文字列、シンボル、呼び出し、参照、注釈、バイト読み取り、ファイルオフセット |
|
||||
| 調査ワークフロー | 14 | アプリ概要、関数ドシエ、ネイティブ API とディスパッチ、一括逆コンパイル、機能トレース、コールパス、コールグラフ、Swift と Objective-C の検出 |
|
||||
| macOS ネイティブ | 7 | Hopper を起動せずに扱える Mach-O メタデータ、コード署名、plist、アーキテクチャ、Swift シンボルのデマングル |
|
||||
| 成果物グラフ | 5 | ディレクトリとパッケージの目録、コンパイル済み Interface Builder ファイル、Apple アセットカタログ、抽出 |
|
||||
| マネージド PE/CLI | 7 | .NET の識別情報、メタデータ、CIL 命令、ネイティブ依存関係、再構築結果のインポート、ビルド比較 |
|
||||
| ファームウェア | 2 | Linux ファームウェア領域の検査と明示的な展開 |
|
||||
| Android APK | 5 | パッケージと manifest の宣言、クラス検索、メンバー一覧、メソッドの逆コンパイル、静的な被参照 |
|
||||
| ブラウザー観察 | 11 | ページ構成、ネットワークメタデータ、スクリプト、ソースマップ、WebMCP の検出、スクリーンショット、キャプチャ比較 |
|
||||
| Electron 解析 | 5 | レンダラーの観察、静的なアプリのマッピング、静的結果と実行時結果の照合 |
|
||||
| JavaScript 実行時 | 2 | Node/Electron Inspector の対象検出、スクリプトの位置、実行コンテキストのイベント |
|
||||
| アプリワークフロー | 13 | 取得した Web スクリプトのエクスポート、Android/Apple の目録の投影、層をまたぐ機能トレース、ビルド比較、過去のソースとの対応付け、静的な戻り値形状の比較、再構築の検証 |
|
||||
| ワークスペースと観察 | 21 | セッション、Evidence バンドル、ナビゲーションの文脈、プロセス・成果物・関数の比較、未解決事項の記録 |
|
||||
|
||||
## ロードマップ
|
||||
|
||||
次の作業はネイティブ対象の検証拡大、JavaScript と .NET のバージョン比較、実行時の観察と再実装の検証です。Setup はエージェント連携と Hopper のインストールを選択できます。他の解析ツールのインストールは今後の作業です。[インストールのロードマップ](docs/roadmap.md)と[解析ツールの評価](docs/provider-evaluation.md)を参照してください。
|
||||
[現在の対応状況](#現在の対応状況)は提供済みの機能です。今後の作業は次のとおりです。
|
||||
|
||||
- **現在:** ツール、プロバイダー、セットアップの選択肢、バージョンの変更に合わせてドキュメントを正確に保ち、Hopper と Ghidra で検証するネイティブバイナリのアーキテクチャと間接呼び出しの範囲を広げます。
|
||||
- **次:** 機能トレースに静的抽出と実行時の観察を追加し、難読化された .NET アセンブリの比較を改善し、プロセス、プロトコル、ファイルシステム、再接続、バージョン比較の範囲を広げます。
|
||||
- **その後:** ブラウザーと Electron のシナリオ操作を増やし、LLDB や Frida などによるネイティブアプリの実行時観察と、他の解析ツールや対象の評価を進めます。
|
||||
|
||||
セットアップでは、すでにエージェント連携と Hopper のインストールを選べます。その他の解析ツールのインストールへの対応は今後の作業です。[インストールのロードマップ](docs/roadmap.md)と[解析ツールの評価](docs/provider-evaluation.md)を参照してください。
|
||||
|
||||
## 他のコーディングエージェントで使う
|
||||
|
||||
Setup は Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf、Devin、OpenCode、Antigravity、GitHub Copilot CLI、Command Code、VS Code に対応します。既存の REA 登録は初期選択され、それ以外の検出済みクライアントは選択が必要です。ローカル MCP サーバーに対応するエージェントは、次の設定でも接続できます。
|
||||
セットアップは Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf、Devin、OpenCode、Antigravity、GitHub Copilot CLI、Command Code、VS Code に対応しています。既存の REA 登録は最初から選択され、それ以外の検出済みエージェントは選択するまで対象になりません。ローカル MCP サーバーに対応するエージェントであれば、次の設定でも接続できます。
|
||||
|
||||
<!-- x-release-please-start-version -->
|
||||
|
||||
@@ -240,29 +280,33 @@ Setup は Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf
|
||||
|
||||
<!-- x-release-please-end -->
|
||||
|
||||
常用する登録では、パッケージのバージョンを 1 つに固定してください。`rea setup` はその固定バージョンを維持し、同梱のスキルも同時に更新します。
|
||||
|
||||
## 仕組み
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Agent["コーディングエージェント"] --> REA["REA<br/>CLI + MCP"]
|
||||
Terminal["ターミナル"] --> REA
|
||||
REA --> Hopper["Hopper 解析操作"]
|
||||
REA --> Hopper["Hopper プロバイダー"]
|
||||
REA --> Ghidra["Ghidra プロバイダー<br/>インベントリ + 関数解析 + 注釈"]
|
||||
REA --> Ida["IDA MCP プロバイダー<br/>attached GUI または headless データベース"]
|
||||
Hopper --> App["あなたのアプリ"]
|
||||
REA --> Ghidra["Ghidra 読み取り専用解析<br/>インベントリ + 関数解析"]
|
||||
Ghidra --> App
|
||||
Ida --> App
|
||||
```
|
||||
|
||||
CLI と MCP サーバーは同じ解析処理を使います。ターミナルコマンドは完了後に自分のブリッジセッションを解放し、エージェントセッションは調査中の接続を維持できます。REA セッションを閉じても、ユーザーが利用中の Hopper は終了しません。
|
||||
CLI と MCP サーバーは、同じアプリケーションワークフローと Evidence の契約を使います。ターミナルのコマンドは完了すると自身のブリッジセッションを解放し、エージェントのセッションは調査中の対象と Evidence を保持できます。REA のセッションを閉じても、利用中の Hopper アプリは終了しません。
|
||||
|
||||
## CLI
|
||||
|
||||
上のエージェントワークフローが、REA を使う最も簡単な方法です。ターミナルから一度だけアプリの概要を調べる場合は、次を実行します。
|
||||
上のエージェントワークフローが、REA を使う最も簡単な方法です。ターミナルからアプリの概要を一度だけ調べる場合は、次を実行します。
|
||||
|
||||
```bash
|
||||
npx -y rea-agents@latest analyze /Applications/Notes.app
|
||||
```
|
||||
|
||||
直接デコンパイルする方法やその他のオプションは、`npx -y rea-agents@latest --help` で確認できます。
|
||||
直接の逆コンパイルやその他のオプションは、`npx -y rea-agents@latest --help` で確認できます。
|
||||
|
||||
グローバルな `rea` コマンドとしてもインストールできます。
|
||||
|
||||
@@ -272,41 +316,49 @@ rea --help
|
||||
rea mcp
|
||||
```
|
||||
|
||||
REA は Mac の `.app` フォルダーを直接開けます。エージェントがアプリを見つけられない場合は、インストール場所を伝えてください。
|
||||
REA は Mac の `.app` フォルダーを直接開けます。エージェントがアプリを名前で見つけられない場合は、インストール場所を伝えてください。
|
||||
|
||||
## Hopper アプリの動作
|
||||
|
||||
REA は必要なときに Hopper を起動します。Hopper のランチャーは内部でアプリをアクティブ化するため、ターゲットを開くと Hopper が前面に出る場合があります。REA はバックグラウンド起動を要求しますが、常に背面に留まる保証はありません。
|
||||
REA は操作に必要になった時点で Hopper を起動します。Hopper のランチャーは内部でアプリをアクティブにするため、REA がバックグラウンドでの起動を要求しても、macOS では Hopper のウィンドウやダイアログが前面に出る場合があります。デモやライセンスの画面では操作が必要になることがあります。
|
||||
|
||||
明示的な形式・アーキテクチャ引数により一般的な FAT / ARM 選択ダイアログを避けますが、別の Hopper / macOS ダイアログは人の操作を必要とする場合があります。セッションを閉じるとブリッジとソケットを削除しますが、ユーザーが利用中の Hopper は終了しません。
|
||||
Hopper は解析リクエストを一度に 1 つずつ処理します。待機をキャンセルしても、Hopper 内ですでに実行中の処理は止まりません。処理中かどうかはセッションが報告します。セッションを閉じると REA のブリッジと一時ソケットディレクトリを削除しますが、利用中の Hopper アプリは終了しません。後片付けを確認できなかった場合、`close_binary` は `cleanup_incomplete` と対象のリソースを報告します。
|
||||
|
||||
## セキュリティモデル
|
||||
|
||||
各セッションはランダムな capability token と現在のユーザーだけが使える Unix ソケットを使用します。Ghidra セッションは隔離された一時プロジェクトも使用し、ユーザー所有の Ghidra プロジェクトを開いたり変更したりしません。これはサンドボックスではなく、同じ OS ユーザー権限で動作する悪意あるプロセスを防御しません。信頼できないバイナリの解析は、現在のユーザー権限で選択されたローカルプロバイダーに委譲されます。脆弱性は [SECURITY.md](SECURITY.md) の非公開手順で報告してください。
|
||||
解析はローカルで実行されます。REA は Hopper や Ghidra とは認証付きのプライベートなローカルソケットで、IDA とは設定済みのローカル MCP 登録を通じて通信します。Ghidra セッションは隔離された一時プロジェクトを使い、ユーザーの Ghidra プロジェクトを開いたり変更したりしません。
|
||||
|
||||
これはサンドボックスではありません。解析ツールと起動した対象は現在のユーザー権限で動作し、同じ OS ユーザー権限で動く悪意のあるプロセスは防げません。Windows Ghidra P0 では、ネイティブの Job Object、保護されたプライベート DACL、ハンドルベースの受け入れ検査を自動的に使います。脆弱性は [SECURITY.md](SECURITY.md) に記載された非公開の手順で報告してください。
|
||||
|
||||
## FAQ
|
||||
|
||||
<details><summary><strong>Hopper を先に起動する必要がありますか?</strong></summary>
|
||||
<details><summary><strong>Hopper を先に起動しておく必要はありますか?</strong></summary>
|
||||
|
||||
いいえ。REA が必要時に起動します。Hopper が起動済みでも使えますが、既存の GUI ドキュメントには接続せず、新しい解析ドキュメントを開きます。
|
||||
いいえ。REA は必要になった時点で Hopper を起動します。すでに起動している Hopper でも使えますが、既存の GUI ドキュメントには接続せず、新しい解析ドキュメントを開きます。
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary><strong>REA に Hopper は含まれますか?</strong></summary>
|
||||
<details><summary><strong>REA に Hopper は含まれていますか?</strong></summary>
|
||||
|
||||
含まれません。Setup で Hopper をインストールできますが、Hopper は独自のライセンスを持つ別製品です。REA は CLI、MCP サーバー、エージェント向けワークフローを提供します。
|
||||
含まれていません。セットアップで Hopper をインストールできますが、Hopper は独自のライセンスを持つ別製品です。REA は、エージェントが Hopper を使えるようにする CLI、MCP サーバー、ワークフローを提供します。
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary><strong>バイナリはアップロードされますか?</strong></summary>
|
||||
<details><summary><strong>REA は Ghidra や Java をインストールしたり同梱したりしますか?</strong></summary>
|
||||
|
||||
REA にホスト型解析サービスはありません。ローカル Unix ソケット経由で Hopper を操作します。エージェントやモデル提供者のデータポリシーは別途確認してください。
|
||||
いいえ。REA は既存の Ghidra のインストールに接続します。[要件](#要件)にある Ghidra と Java のパスを設定してから、セットアップを実行してください。
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary><strong>アプリはアップロードされますか?</strong></summary>
|
||||
|
||||
REA にホスト型の解析サービスはありません。現在のプロバイダーは、成果物の解析と動作のキャプチャをローカルで行います。エージェントやモデルの提供元には独自のデータポリシーがあるため、別途確認してください。
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary><strong>元のソースコードを復元できますか?</strong></summary>
|
||||
|
||||
保証できません。REA は疑似コード、アセンブリ、シンボル、文字列、メタデータ、関係を提供し、エージェントが観察した動作を説明または互換再現できるようにします。
|
||||
元のソースコードを確実に復元できるデコンパイラーはありません。REA はエージェントに疑似コード、アセンブリ、シンボル、文字列、メタデータ、それらの関係を提供し、観察された動作の説明や互換性のある再現に役立てられるようにします。
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ REA retains local observation data and removes only URL userinfo credentials, ex
|
||||
- URL observations preserve the complete local URL, including query values, fragments, duplicate parameters, and their order. Only username/password userinfo is removed; malformed original URL strings remain available with `origin: null`.
|
||||
- DOM snapshots retain node types, node names, value lengths, and attribute names, but not text or attribute values.
|
||||
- Accessibility structure and roles are retained by default, while names and descriptions require `include_accessibility_text: true`.
|
||||
- Network observations retain method, status, MIME type, size, type, initiator stack location, and complete URLs with userinfo credentials removed. Headers are discarded after an allowlisted projection of length/encoding, structured CSP/Link/policy fields, and untrusted agent hints. Cookies and authorization headers are never retained.
|
||||
- Network observations retain method, status, MIME type, size, type, initiator stack location, and complete URLs with userinfo credentials removed. Redirects reported by CDP are attached as ordered `redirects` on the one request ID, with the prior request URL, CDP response URL, method/type, response status/MIME/encoded length, request timestamp, and the next `requestWillBeSent` event timestamp. CDP delivers `redirectResponse` with that next request event, so the latter is an event boundary rather than an exact response timestamp. Redirect response headers and bodies are discarded; hops outside the selected origins are discarded with the request chain. Headers are discarded after an allowlisted projection of length/encoding, structured CSP/Link/policy fields, and untrusted agent hints. Cookies and authorization headers are never retained.
|
||||
- Request/response bodies are not requested or parsed by default. When selected, only allowed-origin JSON media types are read, converted immediately to complete value-free property paths/types, and discarded. `Network.getResponseBody` is never sent unless JSON body shape capture is selected.
|
||||
- Console observations with a stack source retain call type, argument types, timestamp, and source location. When console text capture is selected, already-delivered primitive values are retained verbatim; objects, getters, and remote properties are never expanded.
|
||||
- WebSocket observations retain direction, opcode, and payload byte length. When shape capture is selected, text frames are classified as text or complete value-free JSON shape; binary bytes, hashes, prefixes, and raw frames are never retained.
|
||||
|
||||
@@ -425,3 +425,29 @@ native-value verifier locations are retained until their own proof lanes migrate
|
||||
interactive macOS UI success and real dependency tracing require their respective
|
||||
host/engine workflows. Portable source fixtures and injected call-route tests
|
||||
establish their narrower boundaries, without expanding provider/platform support.
|
||||
|
||||
## Process capture and analyst workflow ownership
|
||||
|
||||
`src/domain/process/` owns scenario/capture validation, portable process trees,
|
||||
trace specifications, observations, comparisons and the canonical process Evidence
|
||||
identity. The standalone contract example lives in `src/contracts/process/`;
|
||||
shared session contracts retain their broader investigation owner.
|
||||
|
||||
`src/process/capture/` owns the actual PTY boundary, terminal rendering, sampling,
|
||||
selected child environment, filesystem snapshots/effects, runtime path admission,
|
||||
event journal, settlement and resource cleanup. It reuses `src/process/` ownership
|
||||
primitives and the Windows host substrate. Its identity import follows the
|
||||
existing pure declaration directly. A source guard rejects outward application,
|
||||
composition and caller imports; no new provider or lifecycle interface is added.
|
||||
|
||||
`src/application/process/` retains Evidence projection and file-backed CLI
|
||||
capture/comparison workflows. Capture helper tests use the forked adapter lane;
|
||||
Evidence/host integration tests use the serial process-boundary lane. Their
|
||||
fixtures and assertions remain unchanged. Installed capability-probe URLs follow
|
||||
the new compiled adapter path, including the optional-dependency failure probe.
|
||||
|
||||
Windows PTY capture retains its existing unavailable outcome because descendant
|
||||
cleanup is not yet verified. Native Job Objects and a PTY binary do not establish
|
||||
that capture workflow. Real POSIX capture/cleanup and installed terminal/PTY module
|
||||
resolution require their actual host and package lanes; portable comparison
|
||||
fixtures establish their separate evidence semantics.
|
||||
|
||||
+12
-7
@@ -68,13 +68,18 @@ selects the npm release, and persistent MCP registrations are pinned to the
|
||||
version that performed setup. Installing newer instructions does not update a
|
||||
running server or its registration.
|
||||
|
||||
The release checked on **2026-10-06** was **4.1.0** (125 MCP tools). That artifact
|
||||
includes main's Windows native control bundle, Android/JADX and firmware
|
||||
tools, Ghidra function annotation workflow, and retained application-Evidence
|
||||
references.
|
||||
Main's catalog describes the current code; a source build or a subsequent
|
||||
release containing later changes is required for anything newer. Package startup alone does not verify a
|
||||
provider's real platform workflow.
|
||||
The release checked on **2026-10-07** was **5.0.0** (133 MCP tools), published
|
||||
from the fixed checkpoint
|
||||
[`b33236ec`](https://github.com/morluto/rea/releases/tag/rea-agents-5.0.0).
|
||||
The public CLI, MCP catalog and target-free session, and isolated update from
|
||||
4.1.0 to 5.0.0 were verified through npm. The artifact includes Windows native
|
||||
controls, Android/JADX and firmware tools, Ghidra function annotations, and
|
||||
retained application-Evidence references.
|
||||
|
||||
Main's catalog describes the current code. A source build or a subsequent
|
||||
release containing changes after this checkpoint is required for newer
|
||||
functionality. Package startup alone does not verify a provider's real
|
||||
platform workflow.
|
||||
|
||||
To check the published version, run `npm view rea-agents dist-tags.latest`.
|
||||
Use the connected server's actual tool list and advertised input schemas for
|
||||
|
||||
@@ -675,9 +675,9 @@
|
||||
"mcp_prompts": 6
|
||||
},
|
||||
"digests": {
|
||||
"tools_sha256": "af7fe81ec689c2a77f04579b6f4beb4abe86aa71d49c4844d8e0f4d7ed7a3a1e",
|
||||
"tools_sha256": "4baede40da9b3bb792e6c35b9b461745ac302973931281be26c8ad7a99f72872",
|
||||
"prompts_sha256": "ef2de8814390b340623c32001610aa2709d90bed51537b6e562ab3abf144ab4d",
|
||||
"combined_sha256": "83b4c3445ed0ec003c0aeefa3188f123d9ae319484f8b1e2b611d2fe278c9c72",
|
||||
"combined_sha256": "ed0b788dd5a6bf9446f840e388822db94b54589a5e294816a3e13bf2bec9f48c",
|
||||
"providers_sha256": "aea15f517c822d80918ccaba57a4fa06557f484cf27b8acbaed4e1a660fac896"
|
||||
}
|
||||
}
|
||||
|
||||
+55
-36
@@ -1,10 +1,10 @@
|
||||
# Releasing from a checkpoint
|
||||
|
||||
REA releases use a frozen source checkpoint. Main can continue accepting
|
||||
REA releases use an explicit source checkpoint. Main can continue accepting
|
||||
changes while a release is tested and published. The Release workflow runs
|
||||
on `release/*` branches; main pushes do not refresh release PRs.
|
||||
only when a maintainer dispatches it; main pushes do not refresh release PRs.
|
||||
|
||||
## 1. Cut the release branch
|
||||
## 1. Select the source
|
||||
|
||||
Choose the next version from the unreleased Conventional Commits, including
|
||||
breaking changes. Record the full source SHA and create `release/VERSION` at
|
||||
@@ -25,43 +25,58 @@ older checkpoint, backport only the release infrastructure first and record
|
||||
that additional commit. Do not merge later implementation changes into the
|
||||
release branch or rebase the candidate onto a moving main.
|
||||
|
||||
The branch push runs Release Please against that branch and opens a PR for
|
||||
the version, changelog, and registry metadata. The generation step normalizes
|
||||
the product catalog. To retry preparation after a failed run, select the same
|
||||
frozen branch explicitly:
|
||||
## 2. Prepare the bot PR
|
||||
|
||||
Run the workflow definition from main and select the frozen source separately:
|
||||
|
||||
```bash
|
||||
gh workflow run release.yml --ref release/5.0.0
|
||||
gh workflow run release.yml --ref main \
|
||||
-f release_branch=release/5.0.0 -f phase=prepare
|
||||
```
|
||||
|
||||
## 2. Review and merge the bot PR
|
||||
Release Please targets that branch and prepares its version, changelog, and
|
||||
registry metadata. The workflow uses an optional `RELEASE_PLEASE_TOKEN`
|
||||
repository secret and otherwise uses the built-in `GITHUB_TOKEN`. A dedicated
|
||||
GitHub App or personal access token can start PR CI automatically. With the
|
||||
built-in token, `opened`, `synchronize`, and `reopened` PR events create runs
|
||||
that require a maintainer with write access to approve them. See
|
||||
[GitHub's workflow trigger documentation](https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/trigger-a-workflow).
|
||||
|
||||
Record the final bot PR head after normalization. Approve GitHub-blocked bot
|
||||
workflow runs for that head when needed. Review the candidate's version,
|
||||
notes, generated metadata, and package contents. Wait for the candidate's CI
|
||||
and relevant real-provider checks. Routine local iterations need focused
|
||||
checks; CI owns full deterministic coverage and platform lanes. If an artifact
|
||||
or real-provider check is unavailable, report that limit before deciding to
|
||||
publish.
|
||||
The generation step normalizes the product catalog. Preparation cannot create
|
||||
a GitHub release or publish a package. Record the final bot PR head after
|
||||
normalization, then approve that head's blocked runs from the PR page. Updates
|
||||
to the bot branch can create new approval-required runs; approval of an older
|
||||
head does not verify the normalized candidate. Wait for the final head's CI.
|
||||
|
||||
Merge the reviewed PR into `release/5.0.0` with its head SHA matched. **This
|
||||
merge authorizes and triggers publication.** Further main commits do not change
|
||||
this candidate. Do not push or merge anything else into the release branch
|
||||
while publication is running. A necessary release fix must be reviewed and
|
||||
tested before the bot PR merges and establishes a new recorded checkpoint.
|
||||
Review the candidate's version, notes, generated metadata, and package
|
||||
contents. Wait for the candidate's CI and relevant real-provider checks.
|
||||
Routine local iterations need focused checks; CI owns full deterministic
|
||||
coverage and platform lanes. If an artifact or real-provider check is
|
||||
unavailable, report that limit before deciding to publish.
|
||||
|
||||
## 3. Verify publication
|
||||
Merge the reviewed PR into `release/5.0.0` with its head SHA matched. Further
|
||||
main commits do not change this candidate. A necessary release fix belongs
|
||||
on the release branch, must be reviewed and tested, and establishes a new
|
||||
recorded checkpoint.
|
||||
|
||||
The merge push runs Release Please again, creates the tag and GitHub release,
|
||||
and publishes npm followed by MCP Registry metadata. Both publishers check
|
||||
out Release Please's exact release SHA. The triggering workflow also runs
|
||||
from that release commit so npm's provenance records the actual source.
|
||||
## 3. Publish the reviewed merge
|
||||
|
||||
Before invoking Release Please, the workflow checks that the release branch
|
||||
still equals the triggering SHA and rejects main or tag refs. Before either
|
||||
registry publish, it rejects a mismatch between the tagged release SHA and
|
||||
the triggering SHA. These checks detect moved branches; maintainers must also
|
||||
keep the release branch frozen throughout publication.
|
||||
After the release PR has merged:
|
||||
|
||||
```bash
|
||||
gh workflow run release.yml --ref release/5.0.0 \
|
||||
-f release_branch=release/5.0.0 -f phase=publish
|
||||
```
|
||||
|
||||
Publication creates the release from the merged bot PR without preparing or
|
||||
updating another PR. Both npm and MCP Registry jobs check out Release Please's
|
||||
exact release SHA. They do not build the current main tip or a mutable branch.
|
||||
The publish dispatch runs from the frozen release branch so npm's provenance
|
||||
records the actual release commit. Before Release Please creates a tag, the
|
||||
workflow accepts only `prepare` or `publish` and requires the selected branch
|
||||
tip to equal the dispatch SHA. Registry jobs run only during `publish`. After
|
||||
a tag exists, a mismatch between that tag's SHA and the dispatch SHA stops
|
||||
both registry publishes.
|
||||
See [npm's provenance implementation](https://github.com/npm/cli/blob/v11.16.0/workspaces/libnpmpublish/lib/provenance.js)
|
||||
for the use of GitHub's workflow ref and commit SHA.
|
||||
|
||||
@@ -78,7 +93,7 @@ Record these outcomes separately:
|
||||
|
||||
A GitHub tag alone does not establish npm or MCP Registry publication.
|
||||
|
||||
## Sync metadata back to main
|
||||
## 4. Sync metadata back to main
|
||||
|
||||
After publication, open a PR from the release branch back to main. Preserve
|
||||
main's later implementation changes and resolve generated-file conflicts by
|
||||
@@ -88,6 +103,9 @@ tag remains in main's ancestry. Keep the released tag unchanged.
|
||||
|
||||
Close any superseded rolling release PR. Future releases repeat the checkpoint
|
||||
procedure from main; never resume automatic release-PR refreshes on main pushes.
|
||||
The generated-metadata workflow stays limited to pull requests into main so it
|
||||
does not push commits onto a frozen candidate. Preparation normalizes
|
||||
`docs/product-catalog.json` on the bot pull request.
|
||||
|
||||
## Partial publication and retries
|
||||
|
||||
@@ -97,7 +115,8 @@ publishing it again. Re-run failed jobs in the original publication run so its
|
||||
release SHA and outputs stay fixed. If only MCP publication failed, retry that
|
||||
job after checking that the npm canary succeeded.
|
||||
|
||||
Do not start a fresh workflow run to repair an already-created release:
|
||||
Release Please will not create the same release again. Do not move the tag,
|
||||
delete the release, or unpublish npm as a retry. A defective public package
|
||||
requires a reviewed correction and a new version.
|
||||
A branch tip that moved after dispatch fails before Release Please creates a
|
||||
tag. Do not dispatch a fresh publish phase to repair an already-created
|
||||
release: Release Please will not create the same release again. Do not move
|
||||
the tag, delete the release, or unpublish npm as a retry. A defective public
|
||||
package requires a reviewed correction and a new version.
|
||||
|
||||
@@ -103,6 +103,15 @@ names remain unchanged.
|
||||
The existing Apple CI job also runs the host Swift-demangling CLI/MCP regression
|
||||
suites, including option-like symbols, carriage returns and multiline rejection.
|
||||
|
||||
Process semantics/tests live in `src/domain/process/`; PTY capture implementation
|
||||
and forked helper tests live in `src/process/capture/`. Evidence projection and
|
||||
file workflows live in `src/application/process/`; producer-backed Evidence/host
|
||||
cases run in the serial `tests/boundary/process/` lane. Installed-package probes
|
||||
load the compiled capture capability owner, including the missing-optional-module
|
||||
case. Real capture tests preserve actual descendant and cleanup checks.
|
||||
The existing Apple job also exercises the relocated filesystem snapshot identity,
|
||||
cancellation and descriptor cleanup regressions on macOS.
|
||||
|
||||
MCP SDK transport tests with recording providers remain integration tests.
|
||||
They are useful for schema drift and failure projection but do not prove that
|
||||
Hopper, Ghidra or another substituted engine works. `verify:package` proves
|
||||
|
||||
@@ -68,12 +68,12 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement
|
||||
58,src/application/SetupHost.ts:unsupported Node,Runtime version cannot run setup,"Install Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+ and rerun setup.",Low risk; exact dependency and retry,Keep current copy,unchanged,verified by setup host tests
|
||||
59,src/application/Setup.ts:unsupported macOS,macOS version is missing or older than 12,Upgrade to macOS 12 or newer.,Low risk; exact support floor,Keep current copy,unchanged,verified by setup host tests
|
||||
60,src/application/Setup.ts:unsupported Linux,Linux distribution is outside supported set,"REA supports Hopper on Ubuntu 24.04+, Fedora 41+, 64-bit Arch Linux, and CachyOS.",Low risk; exact supported alternatives,Keep current copy,unchanged,verified by setup unit test
|
||||
61,src/application/ProcessCli.ts:readJson read,Process scenario or evidence file cannot be read,Raw filesystem exception through CLI framework,Can expose local paths/codes and no recovery,"Process input file could not be read. Check that the path exists and is readable.",implemented,verified by focused CLI test
|
||||
62,src/application/ProcessCli.ts:readJson size,Process scenario or evidence file exceeds 64 MiB,Process JSON input exceeds byte limit,Does not state limit or recovery,"Process input file is too large. Reduce it below 64 MiB, then try again.",implemented,verified by focused sparse-file CLI test
|
||||
63,src/application/ProcessCli.ts:readJson parse,Process scenario or evidence file is malformed JSON,Raw JSON parser exception through CLI framework,Can expose parser implementation and lacks recovery,"Process input file is not valid JSON. Repair the file, then try again.",implemented,verified by focused CLI test
|
||||
64,src/application/ProcessCli.ts:capture scenario parse,Scenario fails Process Capture schema,Raw Zod validation exception through CLI framework,Exposes schema internals and field paths without stable recovery,"Process scenario is invalid. Check its required fields and limits, then try again.",implemented,verified by focused CLI test
|
||||
66,src/application/ProcessCli.ts:wrong evidence,Comparison receives valid Evidence from another workflow,Expected REA process capture Evidence,Uses internal schema name and does not explain recovery,"Capture evidence is not from the current process-capture workflow. Create new capture evidence, then try again.",implemented,verified by focused CLI test
|
||||
67,src/application/ProcessCli.ts:unexpected,Unexpected parser/comparison/canonicalization failure escapes expected classifications,Raw thrown exception through CLI framework,Can expose stack or internal invariant,"Process command could not complete. Check the input files and run `rea doctor`, then try again.",implemented,verified by direct unexpected-cause projection test
|
||||
61,src/application/process/ProcessCli.ts:readJson read,Process scenario or evidence file cannot be read,Raw filesystem exception through CLI framework,Can expose local paths/codes and no recovery,"Process input file could not be read. Check that the path exists and is readable.",implemented,verified by focused CLI test
|
||||
62,src/application/process/ProcessCli.ts:readJson size,Process scenario or evidence file exceeds 64 MiB,Process JSON input exceeds byte limit,Does not state limit or recovery,"Process input file is too large. Reduce it below 64 MiB, then try again.",implemented,verified by focused sparse-file CLI test
|
||||
63,src/application/process/ProcessCli.ts:readJson parse,Process scenario or evidence file is malformed JSON,Raw JSON parser exception through CLI framework,Can expose parser implementation and lacks recovery,"Process input file is not valid JSON. Repair the file, then try again.",implemented,verified by focused CLI test
|
||||
64,src/application/process/ProcessCli.ts:capture scenario parse,Scenario fails Process Capture schema,Raw Zod validation exception through CLI framework,Exposes schema internals and field paths without stable recovery,"Process scenario is invalid. Check its required fields and limits, then try again.",implemented,verified by focused CLI test
|
||||
66,src/application/process/ProcessCli.ts:wrong evidence,Comparison receives valid Evidence from another workflow,Expected REA process capture Evidence,Uses internal schema name and does not explain recovery,"Capture evidence is not from the current process-capture workflow. Create new capture evidence, then try again.",implemented,verified by focused CLI test
|
||||
67,src/application/process/ProcessCli.ts:unexpected,Unexpected parser/comparison/canonicalization failure escapes expected classifications,Raw thrown exception through CLI framework,Can expose stack or internal invariant,"Process command could not complete. Check the input files and run `rea doctor`, then try again.",implemented,verified by direct unexpected-cause projection test
|
||||
68,src/server/registerEvidenceTools.ts:object guard,Validated provider-tool input is unexpectedly not an object,Provider tool input was not an object,Exposes provider architecture and implies caller can fix impossible state,Remove the impossible duplicate guard and parse through the declared JSON-object schema.,removed,verified by typecheck and MCP integration tests
|
||||
69,src/server/registerEnhancedTools.ts:object guard,Validated enhanced-tool input is unexpectedly not an object,Validated enhanced input was not a JSON object,Exposes validation/JSON implementation detail,Remove the impossible duplicate guard and parse through the declared JSON-object schema.,removed,verified by typecheck and enhanced MCP integration tests
|
||||
70,src/server/registerOfficialTools.ts:object guard,Validated direct-tool input is unexpectedly not an object,Validated MCP tool input was not a JSON object,Exposes MCP validation implementation detail,Remove the impossible duplicate guard and parse through the declared JSON-object schema.,removed,verified by typecheck and official MCP integration tests
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"manifest_id": "ecm_880356f1ceceb9e29b43e3e89f7a326aa83132e1e1fae0573245848ba714f404",
|
||||
"manifest_id": "ecm_4d1cf621854fe1f40698ef60f340fb99263edb0e99218c865b09cb26112d2547",
|
||||
"verifier": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
@@ -13,7 +13,7 @@
|
||||
"skill_digests": [
|
||||
{
|
||||
"skill_id": "reverse-engineer-anything",
|
||||
"sha256": "97dc167b6e0d41ac69953501c96a918319fa6e4b3be0affe2fc2119294e7101d"
|
||||
"sha256": "1d816388b86e50fff47db599200968788a4bf2272f40d5bdd3bb51aae7f5a9b4"
|
||||
}
|
||||
],
|
||||
"claims": [
|
||||
|
||||
+3
-3
@@ -66,11 +66,11 @@
|
||||
"metadata:generate": "node scripts/generate-package-metadata.mjs",
|
||||
"metadata:check": "node scripts/generate-package-metadata.mjs --check",
|
||||
"mcp-registry:check": "node scripts/verify-mcp-registry-metadata.mjs",
|
||||
"mcp-catalog:generate": "npm run build:cached && node scripts/generate-mcp-tool-catalog.mjs",
|
||||
"mcp-catalog:generate": "npm run build:cached",
|
||||
"mcp-catalog:check": "node scripts/generate-mcp-tool-catalog.mjs --check",
|
||||
"prebuild": "npm run deps:check && npm run metadata:check",
|
||||
"build": "node scripts/run-exclusive.mjs build npm run build:unlocked",
|
||||
"build:unlocked": "node scripts/clean-build-output.mjs && tsc -p tsconfig.build.json",
|
||||
"build:unlocked": "node scripts/clean-build-output.mjs && tsc -p tsconfig.build.json && node scripts/generate-mcp-tool-catalog.mjs",
|
||||
"precheck": "npm run deps:check",
|
||||
"check": "turbo run typecheck lint format:check knip metadata:check",
|
||||
"precheck:fast": "npm run deps:check",
|
||||
@@ -165,7 +165,7 @@
|
||||
"docs:build": "vitepress build docs",
|
||||
"docs:preview": "vitepress preview docs",
|
||||
"docs:generate": "node scripts/run-exclusive.mjs docs npm run docs:generate:unlocked",
|
||||
"docs:generate:unlocked": "npm run metadata:generate && npm run build:cached && node scripts/generate-mcp-tool-catalog.mjs && node scripts/generate-skill-metadata.mjs && node scripts/generate-product-catalog.mjs && node scripts/generate-completion-ledger.mjs && node scripts/generate-error-schema.mjs",
|
||||
"docs:generate:unlocked": "npm run metadata:generate && npm run build:cached && node scripts/generate-skill-metadata.mjs && node scripts/generate-product-catalog.mjs && node scripts/generate-completion-ledger.mjs && node scripts/generate-error-schema.mjs",
|
||||
"docs:check": "node scripts/run-exclusive.mjs docs turbo run docs:check:built",
|
||||
"docs:check:built": "node scripts/generate-package-metadata.mjs --check && node scripts/generate-mcp-tool-catalog.mjs --check && node scripts/generate-skill-metadata.mjs --check && node scripts/generate-product-catalog.mjs --check && node scripts/generate-completion-ledger.mjs --check && node scripts/check-doc-facts.mjs && node scripts/generate-error-schema.mjs --check",
|
||||
"docs:error-schema": "npm run build:cached && node scripts/generate-error-schema.mjs",
|
||||
|
||||
@@ -17,8 +17,9 @@ for (const argument of arguments_)
|
||||
throw new Error(`Unknown MCP tool catalog option: ${argument}`);
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
// The checked-in catalog is a portable package artifact. Keep its canonical
|
||||
// provider metadata independent of the machine running the generator.
|
||||
// The build-generated catalog is a portable package artifact that is not
|
||||
// committed to git. Keep its canonical provider metadata independent of the
|
||||
// machine running the generator. It is regenerated by `npm run build`.
|
||||
const CATALOG_PLATFORM = "darwin";
|
||||
const sessionToolNames = new Set([
|
||||
...TOOL_CONTRACTS.filter(({ kind }) => kind === "session").map(
|
||||
|
||||
@@ -75,7 +75,7 @@ const SOURCE_PATHS = {
|
||||
evidenceBundle: "dist/domain/evidenceBundle.js",
|
||||
evidenceCompletion: "dist/domain/evidenceCompletionLedger.js",
|
||||
completionGeneration: "dist/domain/completionLedgerGeneration.js",
|
||||
processCapture: "dist/domain/processCapture.js",
|
||||
processCapture: "dist/domain/process/processCapture.js",
|
||||
analysisSnapshot: "dist/domain/analysisSnapshot.js",
|
||||
artifactGraph: "dist/domain/artifactGraph.js",
|
||||
browserObservation: "dist/domain/browserObservation.js",
|
||||
|
||||
@@ -33,7 +33,7 @@ export const ensureGeneratedFile = async ({
|
||||
return { changed: false };
|
||||
if (check)
|
||||
throw new Error(
|
||||
`${path} is missing or stale. Run \`${generateCommand}\` and commit the result.`,
|
||||
`${path} is missing or stale. Run \`${generateCommand}\` to regenerate it; commit the result if the file is tracked.`,
|
||||
);
|
||||
await writeFile(path, preserveLineEndings(source, existing), "utf8");
|
||||
return { changed: true };
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface ModuleBoundaryViolation {
|
||||
readonly target: string;
|
||||
readonly boundary:
|
||||
| "artifact-acquisition"
|
||||
| "process-capture"
|
||||
| "pure-layer"
|
||||
| "application-composition"
|
||||
| "provider-construction"
|
||||
|
||||
@@ -60,6 +60,14 @@ const failedBoundary = (file, target) => {
|
||||
if (!file.startsWith("src/") || !target.startsWith("src/")) return undefined;
|
||||
const layer = file.split("/")[1];
|
||||
const targetLayer = target.split("/")[1];
|
||||
if (
|
||||
file.startsWith("src/process/capture/") &&
|
||||
(["application", "composition", "server", "cli", "main"].includes(
|
||||
targetLayer,
|
||||
) ||
|
||||
/^src\/(?:cli|main)\./u.test(target))
|
||||
)
|
||||
return "process-capture";
|
||||
if (
|
||||
file.startsWith("src/application/javascript/") &&
|
||||
/^src\/artifacts\/(?:Asar|Directory)ArtifactReader\.(?:js|ts)$/u.test(
|
||||
|
||||
@@ -31,7 +31,7 @@ export async function verifyPackageInstall({
|
||||
const processCaptureCapabilityUrl = pathToFileURL(
|
||||
join(
|
||||
prefix,
|
||||
"lib/node_modules/rea-agents/dist/application/ProcessCaptureCapability.js",
|
||||
"lib/node_modules/rea-agents/dist/process/capture/ProcessCaptureCapability.js",
|
||||
),
|
||||
).href;
|
||||
const processCaptureCapability = json(
|
||||
@@ -77,7 +77,7 @@ export async function verifyPackageInstall({
|
||||
const noOptionalCapabilityUrl = pathToFileURL(
|
||||
join(
|
||||
noOptionalPrefix,
|
||||
"lib/node_modules/rea-agents/dist/application/ProcessCaptureCapability.js",
|
||||
"lib/node_modules/rea-agents/dist/process/capture/ProcessCaptureCapability.js",
|
||||
),
|
||||
).href;
|
||||
const noOptionalCapability = json(
|
||||
|
||||
@@ -4,7 +4,7 @@ description: Reverse engineer native, managed, Electron/JavaScript, packaged, fi
|
||||
metadata:
|
||||
version: "27"
|
||||
tool_count: 133
|
||||
catalog_digest: "83b4c3445ed0ec003c0aeefa3188f123d9ae319484f8b1e2b611d2fe278c9c72"
|
||||
catalog_digest: "ed0b788dd5a6bf9446f840e388822db94b54589a5e294816a3e13bf2bec9f48c"
|
||||
---
|
||||
|
||||
# REA
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
EvidenceIntegrityError,
|
||||
} from "../domain/evidenceErrors.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import { parseProcessCapture } from "../domain/processCapture.js";
|
||||
import { parseProcessCapture } from "../domain/process/processCapture.js";
|
||||
import { readJsonFile, writeTextFile } from "./JsonFiles.js";
|
||||
|
||||
type EvidenceReadFailure = EvidenceFileError | EvidenceIntegrityError;
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
parseEvidenceBundle,
|
||||
type EvidenceBundle,
|
||||
} from "../domain/evidenceBundle.js";
|
||||
import { parseProcessCapture } from "../domain/processCapture.js";
|
||||
import { parseProcessCapture } from "../domain/process/processCapture.js";
|
||||
import type {
|
||||
ReconstructionObligation,
|
||||
ReviewedReconstructionObligation,
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { PROCESS_PROVIDER } from "./ProcessEvidence.js";
|
||||
import { PROCESS_PROVIDER } from "./process/ProcessEvidence.js";
|
||||
import {
|
||||
buildReconstructionObligationLedgerEvidenceValidated,
|
||||
resolveReconstructionObligationLedgerRequest,
|
||||
} from "./ReconstructionObligationLedgerService.js";
|
||||
import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../domain/processCapture.fixture.js";
|
||||
import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../domain/process/processCapture.fixture.js";
|
||||
import { createEvidence, type Evidence } from "../domain/evidence.js";
|
||||
import { createEvidenceBundle } from "../domain/evidenceBundle.js";
|
||||
import { jsonValueSchema } from "../domain/jsonValue.js";
|
||||
import { processCaptureSchema } from "../domain/processCapture.js";
|
||||
import { processCaptureSchema } from "../domain/process/processCapture.js";
|
||||
import {
|
||||
reconstructionObligationLedgerSchema,
|
||||
type ReconstructionObligationLedgerInput,
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { PROCESS_PROVIDER } from "./ProcessEvidence.js";
|
||||
import { PROCESS_PROVIDER } from "./process/ProcessEvidence.js";
|
||||
import {
|
||||
buildReconstructionObligationLedgerEvidenceValidated,
|
||||
resolveReconstructionObligationLedgerRequest,
|
||||
} from "./ReconstructionObligationLedgerService.js";
|
||||
import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../domain/processCapture.fixture.js";
|
||||
import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../domain/process/processCapture.fixture.js";
|
||||
import { createEvidence, type Evidence } from "../domain/evidence.js";
|
||||
import { createEvidenceBundle } from "../domain/evidenceBundle.js";
|
||||
import { jsonValueSchema } from "../domain/jsonValue.js";
|
||||
import { processCaptureSchema } from "../domain/processCapture.js";
|
||||
import { processCaptureSchema } from "../domain/process/processCapture.js";
|
||||
import {
|
||||
reconstructionObligationLedgerSchema,
|
||||
type ReconstructionObligationLedgerInput,
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import {
|
||||
probeProcessCaptureCapability,
|
||||
type ProcessCaptureCapability,
|
||||
} from "./ProcessCaptureCapability.js";
|
||||
} from "../process/capture/ProcessCaptureCapability.js";
|
||||
|
||||
/** One named Windows host observation without inferred authority. */
|
||||
export type WindowsCapabilityOutcome =
|
||||
|
||||
@@ -43,7 +43,6 @@ const input = (
|
||||
overrides: Partial<RecordUnknownInput> = {},
|
||||
): RecordUnknownInput =>
|
||||
recordUnknownInputSchema.parse({
|
||||
approved: true,
|
||||
question,
|
||||
severity: "high",
|
||||
domain: "protocol",
|
||||
@@ -60,7 +59,6 @@ const update = (
|
||||
overrides: Readonly<Record<string, unknown>> = {},
|
||||
) =>
|
||||
updateUnknownInputSchema.parse({
|
||||
approved: true,
|
||||
unknown_id: unknown.unknown_id,
|
||||
expected_revision: unknown.revision,
|
||||
status: "investigating",
|
||||
|
||||
@@ -129,7 +129,6 @@ describe("evidence ledger metadata imports", () => {
|
||||
});
|
||||
const created = ledger.recordUnknown(
|
||||
recordUnknownInputSchema.parse({
|
||||
approved: true,
|
||||
question: "Which path is current?",
|
||||
severity: "high",
|
||||
domain: "fixture",
|
||||
@@ -241,7 +240,6 @@ describe("evidence bundle imports", () => {
|
||||
result: { action: "record" },
|
||||
});
|
||||
const unknown = recordUnknownInputSchema.parse({
|
||||
approved: true,
|
||||
question: "What remains unresolved?",
|
||||
severity: "high",
|
||||
domain: "atomic-test",
|
||||
|
||||
@@ -101,9 +101,18 @@ describe("investigation record ownership", () => {
|
||||
);
|
||||
if (!created.ok) throw created.error;
|
||||
const input = updateUnknownInputSchema.parse({
|
||||
...created.value,
|
||||
unknown_id: created.value.unknown_id,
|
||||
expected_revision: created.value.revision,
|
||||
status: "investigating",
|
||||
severity: created.value.severity,
|
||||
supporting_evidence_ids: created.value.supporting_evidence_ids,
|
||||
contradicting_evidence_ids: created.value.contradicting_evidence_ids,
|
||||
required_authority: created.value.required_authority,
|
||||
required_confidence: created.value.required_confidence,
|
||||
required_environment: created.value.required_environment,
|
||||
recommended_probes: created.value.recommended_probes,
|
||||
relationships: created.value.relationships,
|
||||
resolution: created.value.resolution,
|
||||
});
|
||||
const updated = records.updateUnknown(input, target);
|
||||
expect(updated).toMatchObject({
|
||||
|
||||
@@ -47,6 +47,8 @@ describe("JavaScript application failure diagnostics", () => {
|
||||
const inputPath = await createTestTempDirectory("rea-js-failure-");
|
||||
await writeFile(join(inputPath, "main.js"), "export const value = 1;\n");
|
||||
const cause = new RangeError("Invalid string length");
|
||||
// Retain the original failure locally, but never serialize its object graph.
|
||||
cause.cause = cause;
|
||||
const result = await analyzeJavaScriptApplication(
|
||||
{ input_path: inputPath, format: "directory" },
|
||||
{
|
||||
@@ -72,6 +74,8 @@ describe("JavaScript application failure diagnostics", () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const projection = projectAnalysisError(result.error);
|
||||
expect(JSON.parse(JSON.stringify(projection))).toEqual(projection);
|
||||
});
|
||||
|
||||
it("keeps filesystem failures distinct from engine failures", async () => {
|
||||
@@ -119,4 +123,35 @@ describe("JavaScript application failure diagnostics", () => {
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each([new Error("x".repeat(10_000)), "y".repeat(10_000)])(
|
||||
"preserves long failure messages through JSON projection (%#)",
|
||||
async (cause) => {
|
||||
const inputPath = await createTestTempDirectory("rea-js-oversized-");
|
||||
await writeFile(join(inputPath, "main.js"), "export const value = 1;\n");
|
||||
const message = cause instanceof Error ? cause.message : cause;
|
||||
const result = await analyzeJavaScriptApplication(
|
||||
{ input_path: inputPath, format: "directory" },
|
||||
{
|
||||
progress: {
|
||||
report: async (event) => {
|
||||
if (event.terminal) throw cause;
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected analysis failure");
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
code: "execution_failure",
|
||||
details: {
|
||||
diagnostics: {
|
||||
error_message: message,
|
||||
},
|
||||
},
|
||||
});
|
||||
const projection = projectAnalysisError(result.error);
|
||||
expect(JSON.parse(JSON.stringify(projection))).toEqual(projection);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { buildJavaScriptSemanticGraph } from "./JavaScriptSemanticGraphBuilder.js";
|
||||
import {
|
||||
buildJavaScriptSemanticGraph,
|
||||
SEMANTIC_GRAPH_FILE_NODE_CEILING,
|
||||
SEMANTIC_GRAPH_NODE_CEILING,
|
||||
} from "./JavaScriptSemanticGraphBuilder.js";
|
||||
import {
|
||||
addSemanticGraphNode,
|
||||
constructSemanticGraphNode,
|
||||
createSemanticGraphProjectionState,
|
||||
} from "./JavaScriptSemanticGraphConstruction.js";
|
||||
import type { JavaScriptArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js";
|
||||
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
|
||||
import { queryJavaScriptSemanticGraph } from "../../domain/javascript/javascriptSemanticQuery.js";
|
||||
@@ -287,6 +296,31 @@ const graphFor = (source: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
const semanticNodeFor = (roleKey: string) => {
|
||||
const file: JavaScriptArtifactFile = {
|
||||
path: "budget.js",
|
||||
container_sha256: SHA256,
|
||||
sha256: SHA256,
|
||||
bytes: 0,
|
||||
inventory_artifact_id: `art_${SHA256}`,
|
||||
kind: "javascript",
|
||||
unpacked: false,
|
||||
text: { included: true, value: "" },
|
||||
};
|
||||
const state = createSemanticGraphProjectionState({ nodes: [] });
|
||||
return constructSemanticGraphNode(
|
||||
file,
|
||||
{
|
||||
kind: "expression",
|
||||
roleKey,
|
||||
location: null,
|
||||
label: roleKey,
|
||||
functionNodeId: null,
|
||||
},
|
||||
state,
|
||||
);
|
||||
};
|
||||
|
||||
it.each([
|
||||
"const value = consume(Promise.resolve(1));",
|
||||
"async function run() { await consume(Promise.resolve(1)); }",
|
||||
@@ -428,3 +462,75 @@ it.each([
|
||||
expect(query.relations).toContainEqual(relation);
|
||||
expect(query.nodes.map(({ node_id }) => node_id)).toContain(promise?.node_id);
|
||||
});
|
||||
|
||||
it("bounds semantic node projection and reports the ceiling in coverage", () => {
|
||||
// A bundled vendor library can emit tens of thousands of semantic nodes on its
|
||||
// own (measured: 135,286 from one minified editor bundle). Without a ceiling the
|
||||
// projection exhausts the heap and the result cannot be serialized. The bound
|
||||
// must be reported through coverage rather than dropping data silently.
|
||||
const statements = Array.from(
|
||||
{ length: 4_000 },
|
||||
(_, index) =>
|
||||
`const value${index} = { a: ${index}, b: [${index}, ${index + 1}] };`,
|
||||
).join("\n");
|
||||
const graph = graphFor(statements);
|
||||
|
||||
expect(graph.nodes.length).toBeLessThanOrEqual(SEMANTIC_GRAPH_NODE_CEILING);
|
||||
expect(graph.coverage.truncated).toBe(true);
|
||||
expect(graph.coverage.status).toBe("partial");
|
||||
expect(graph.coverage.omitted_nodes).toBeNull();
|
||||
expect(graph.coverage.omitted_relations).toBeNull();
|
||||
expect(
|
||||
graph.coverage.families.every(
|
||||
({ omitted_relations }) => omitted_relations === null,
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
graph.coverage.limits.some(
|
||||
({ name }) => name === "semantic_graph_node_ceiling",
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("leaves a small application complete and untruncated", () => {
|
||||
const graph = graphFor("const answer = 40 + 2;");
|
||||
expect(graph.coverage.truncated).toBe(false);
|
||||
expect(graph.coverage.omitted_nodes).toBe(0);
|
||||
expect(graph.coverage.omitted_relations).toBe(0);
|
||||
expect(graph.coverage.limits).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not mark a file truncated when it exactly fills its budget", () => {
|
||||
// Each unbound reference projects one expression, plus the module root.
|
||||
const graph = graphFor(
|
||||
"external;\n".repeat(SEMANTIC_GRAPH_FILE_NODE_CEILING - 1),
|
||||
);
|
||||
expect(graph.nodes).toHaveLength(SEMANTIC_GRAPH_FILE_NODE_CEILING);
|
||||
expect(graph.coverage).toMatchObject({
|
||||
truncated: false,
|
||||
omitted_nodes: 0,
|
||||
omitted_relations: 0,
|
||||
limits: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("only records node loss when the budget rejects a new identity", () => {
|
||||
// The truncation signal must record blocked node creation, not a zero
|
||||
// remaining budget: a file using exactly its share drops nothing.
|
||||
const state = createSemanticGraphProjectionState({ nodes: [] });
|
||||
state.fileNodeBudget = 2;
|
||||
state.fileNodesDropped = false;
|
||||
const first = addSemanticGraphNode(state, semanticNodeFor("first"));
|
||||
const second = addSemanticGraphNode(state, semanticNodeFor("second"));
|
||||
expect(first).not.toBeNull();
|
||||
expect(second).not.toBeNull();
|
||||
expect(state.fileNodeBudget).toBe(0);
|
||||
expect(state.fileNodesDropped).toBe(false);
|
||||
|
||||
expect(addSemanticGraphNode(state, semanticNodeFor("first"))).toBe(first);
|
||||
expect(state.fileNodesDropped).toBe(false);
|
||||
|
||||
const blocked = addSemanticGraphNode(state, semanticNodeFor("third"));
|
||||
expect(blocked).toBeNull();
|
||||
expect(state.fileNodesDropped).toBe(true);
|
||||
});
|
||||
|
||||
@@ -74,7 +74,35 @@ interface FileContext extends SemanticFlowProjectionContext {
|
||||
readonly callResolutions: ReadonlyMap<string, "candidate" | "resolved">;
|
||||
}
|
||||
|
||||
/** Project per-file semantic IR into an artifact-bound companion graph. */
|
||||
/**
|
||||
* Resource-safety ceiling on retained semantic nodes.
|
||||
*
|
||||
* The semantic projection emits one node per AST expression, call site, binding
|
||||
* and property slot. That is unbounded in input size. A small tree of five files
|
||||
* already yields on the order of 170,000 nodes, and larger trees yield millions,
|
||||
* exhausting the heap before the result can be serialized.
|
||||
*
|
||||
* This is a real resource-safety constraint, not a presentation preference, so
|
||||
* the ceiling is applied and reported through the graph's own `coverage` fields
|
||||
* (`truncated`, `omitted_nodes`, `omitted_relations`, `limits`) rather than
|
||||
* silently dropping data.
|
||||
*
|
||||
* Sizing: a retained semantic node costs roughly 3 KB once serialized, and V8
|
||||
* additionally caps a single string at 512 MB. The result is serialized after the
|
||||
* object graph is built and validated, so both phases must fit together. A single
|
||||
* minified vendor bundle can account for most of a tree's nodes, so a whole-tree
|
||||
* ceiling without a per-file ceiling does not bound the work. Both are applied.
|
||||
*/
|
||||
export const SEMANTIC_GRAPH_NODE_CEILING = 100_000;
|
||||
|
||||
/**
|
||||
* Per-file share of the semantic node budget.
|
||||
*
|
||||
* Bound each source file independently so one large bundled library cannot
|
||||
* consume the whole tree's budget and starve the application's own modules.
|
||||
*/
|
||||
export const SEMANTIC_GRAPH_FILE_NODE_CEILING = 20_000;
|
||||
|
||||
export const buildJavaScriptSemanticGraph = ({
|
||||
rootArtifactSha256,
|
||||
applicationGraph,
|
||||
@@ -82,11 +110,27 @@ export const buildJavaScriptSemanticGraph = ({
|
||||
}: BuilderInput): JavaScriptSemanticGraph => {
|
||||
const state = emptyState(applicationGraph);
|
||||
const fingerprints: JavaScriptSemanticGraph["fingerprints"][number][] = [];
|
||||
let truncatedFiles = 0;
|
||||
for (const analyzed of analysis.files) {
|
||||
if (analyzed.semantic === null) continue;
|
||||
if (state.nodes.size >= SEMANTIC_GRAPH_NODE_CEILING) {
|
||||
truncatedFiles += 1;
|
||||
continue;
|
||||
}
|
||||
const remainingTreeBudget = SEMANTIC_GRAPH_NODE_CEILING - state.nodes.size;
|
||||
state.fileNodeBudget = Math.min(
|
||||
SEMANTIC_GRAPH_FILE_NODE_CEILING,
|
||||
remainingTreeBudget,
|
||||
);
|
||||
state.fileNodesDropped = false;
|
||||
fingerprints.push(
|
||||
...projectFile(analyzed.file, analyzed.semantic.ir, state),
|
||||
);
|
||||
// Nodes were dropped only if the budget actually blocked creation. A file
|
||||
// that exactly fills its share ends with a zero budget without dropping
|
||||
// anything, so the remaining budget alone cannot decide truncation.
|
||||
if (state.fileNodesDropped) truncatedFiles += 1;
|
||||
state.fileNodeBudget = null;
|
||||
}
|
||||
if (state.roots.size === 0) addFallbackRoot(rootArtifactSha256, state);
|
||||
const unknowns = [...state.unknowns.values()];
|
||||
@@ -100,11 +144,20 @@ export const buildJavaScriptSemanticGraph = ({
|
||||
fingerprints,
|
||||
unknowns,
|
||||
coverage: {
|
||||
status: "unknown",
|
||||
truncated: false,
|
||||
omitted_nodes: 0,
|
||||
omitted_relations: 0,
|
||||
limits: [],
|
||||
status: truncatedFiles > 0 ? "partial" : "unknown",
|
||||
truncated: truncatedFiles > 0,
|
||||
omitted_nodes: truncatedFiles > 0 ? null : 0,
|
||||
omitted_relations: truncatedFiles > 0 ? null : 0,
|
||||
limits:
|
||||
truncatedFiles > 0
|
||||
? [
|
||||
{
|
||||
name: "semantic_graph_node_ceiling",
|
||||
value: SEMANTIC_GRAPH_NODE_CEILING,
|
||||
unit: "items" as const,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
families: JAVASCRIPT_SEMANTIC_RELATION_FAMILIES.map((family) => ({
|
||||
family,
|
||||
status: semanticFamilyStatus(family, analysis),
|
||||
@@ -112,7 +165,7 @@ export const buildJavaScriptSemanticGraph = ({
|
||||
(relation) =>
|
||||
JAVASCRIPT_SEMANTIC_RELATION_FAMILY[relation.relation] === family,
|
||||
).length,
|
||||
omitted_relations: 0,
|
||||
omitted_relations: truncatedFiles > 0 ? null : 0,
|
||||
unknown_ids: unknowns
|
||||
.filter((unknown) => unknown.family === family)
|
||||
.map(({ unknown_id: identifier }) => identifier),
|
||||
|
||||
@@ -26,6 +26,24 @@ export interface SemanticGraphProjectionState {
|
||||
readonly unknowns: Map<string, JavaScriptSemanticGraphUnknown>;
|
||||
readonly roots: Set<string>;
|
||||
readonly applicationNodeIdsByLocation: ReadonlyMap<string, readonly string[]>;
|
||||
/**
|
||||
* Remaining node budget for the file currently being projected.
|
||||
*
|
||||
* The projection emits one node per AST expression, call site, binding and
|
||||
* property slot, which is unbounded in file size: one bundled vendor library
|
||||
* measured 135,286 nodes on its own. `null` means unbounded; a number is the
|
||||
* number of nodes this file may still add. Exhausting it stops node creation
|
||||
* for that file and is reported through the graph's `coverage` fields.
|
||||
*/
|
||||
fileNodeBudget?: number | null;
|
||||
/**
|
||||
* Whether the file currently being projected lost any nodes to its budget.
|
||||
*
|
||||
* A file that exactly fills its share ends with a zero budget without
|
||||
* dropping anything, so callers must consult this flag rather than the
|
||||
* remaining budget to decide whether coverage is truncated.
|
||||
*/
|
||||
fileNodesDropped?: boolean;
|
||||
}
|
||||
|
||||
/** Input for one exact artifact-version semantic node. */
|
||||
@@ -90,6 +108,14 @@ export const addSemanticGraphNode = (
|
||||
): JavaScriptSemanticGraphNode | null => {
|
||||
const existing = state.nodes.get(node.node_id);
|
||||
if (existing !== undefined) return existing;
|
||||
const budget = state.fileNodeBudget;
|
||||
if (typeof budget === "number") {
|
||||
if (budget <= 0) {
|
||||
state.fileNodesDropped = true;
|
||||
return null;
|
||||
}
|
||||
state.fileNodeBudget = budget - 1;
|
||||
}
|
||||
state.nodes.set(node.node_id, node);
|
||||
return node;
|
||||
};
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
import { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import { AnalysisInputError } from "../domain/analysisErrorCore.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import { createEvidence, parseEvidence } from "../domain/evidence.js";
|
||||
import { jsonValueSchema } from "../domain/jsonValue.js";
|
||||
import { projectInputIssues } from "../domain/inputIssueProjection.js";
|
||||
import { processTraceSpecificationSchema } from "../domain/processTraceComparison.js";
|
||||
import { processScenarioSchema } from "../domain/processScenario.js";
|
||||
import { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import { AnalysisInputError } from "../../domain/analysisErrorCore.js";
|
||||
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
|
||||
import { createEvidence, parseEvidence } from "../../domain/evidence.js";
|
||||
import { jsonValueSchema } from "../../domain/jsonValue.js";
|
||||
import { projectInputIssues } from "../../domain/inputIssueProjection.js";
|
||||
import { processTraceSpecificationSchema } from "../../domain/process/processTraceComparison.js";
|
||||
import { processScenarioSchema } from "../../domain/process/processScenario.js";
|
||||
import {
|
||||
compareProcessCaptures,
|
||||
parseProcessCapture,
|
||||
} from "../domain/processCapture.js";
|
||||
import { captureProcessScenario } from "./ProcessHarness.js";
|
||||
} from "../../domain/process/processCapture.js";
|
||||
import { captureProcessScenario } from "../../process/capture/ProcessHarness.js";
|
||||
import {
|
||||
PROCESS_PROVIDER,
|
||||
createProcessCaptureEvidence,
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import { createEvidence } from "../domain/evidence.js";
|
||||
import { PROCESS_PROVIDER } from "../domain/processEvidenceProvider.js";
|
||||
import { jsonValueSchema } from "../domain/jsonValue.js";
|
||||
import type { JsonValue } from "../../domain/jsonValue.js";
|
||||
import { createEvidence } from "../../domain/evidence.js";
|
||||
import { PROCESS_PROVIDER } from "../../domain/process/processEvidenceProvider.js";
|
||||
import { jsonValueSchema } from "../../domain/jsonValue.js";
|
||||
import type {
|
||||
ProcessCapture,
|
||||
ProcessScenario,
|
||||
} from "../domain/processCapture.js";
|
||||
} from "../../domain/process/processCapture.js";
|
||||
|
||||
export { PROCESS_PROVIDER } from "../domain/processEvidenceProvider.js";
|
||||
export { PROCESS_PROVIDER } from "../../domain/process/processEvidenceProvider.js";
|
||||
|
||||
/** Project one process scenario into secret-free Evidence parameters. */
|
||||
const processEvidenceParameters = (
|
||||
@@ -10,7 +10,10 @@ import {
|
||||
cdpStringValue,
|
||||
type UnknownRecord,
|
||||
} from "./CdpCaptureValues.js";
|
||||
import { requestBodyShape } from "./CdpCaptureEventBodyShapes.js";
|
||||
import {
|
||||
requestBodyShape,
|
||||
updateResponseBodyShape,
|
||||
} from "./CdpCaptureEventBodyShapes.js";
|
||||
import {
|
||||
consolePrimitive,
|
||||
decodeBase64,
|
||||
@@ -19,10 +22,11 @@ import {
|
||||
firstCallFrame,
|
||||
initiatorLocation,
|
||||
integerOrNull,
|
||||
isJsonMediaType,
|
||||
isMainFrameNavigation,
|
||||
} from "./CdpCaptureEventHelpers.js";
|
||||
import type { CdpCaptureEventsState } from "./CdpCaptureEventState.js";
|
||||
import type { CapturedScript } from "./CdpCaptureEventTypes.js";
|
||||
import type { CapturedScript, NetworkState } from "./CdpCaptureEventTypes.js";
|
||||
|
||||
export const handleExecutionContextCreated = (
|
||||
state: CdpCaptureEventsState,
|
||||
@@ -120,9 +124,11 @@ export const handleRequestWillBeSent = (
|
||||
state.completeness.exclude("network_requests", "invalid_protocol_value");
|
||||
return;
|
||||
}
|
||||
if (state.malformedRedirectRequestIds.has(requestId)) return;
|
||||
if (request === undefined) {
|
||||
state.completeness.exclude("network_requests", "invalid_protocol_value");
|
||||
state.network.delete(requestId);
|
||||
state.networkRequestTimestamps.delete(requestId);
|
||||
return;
|
||||
}
|
||||
const sanitized = allowedSanitizedUrl(request.url, state.allowedOrigins);
|
||||
@@ -132,8 +138,60 @@ export const handleRequestWillBeSent = (
|
||||
exclusionReasonForUrl(cdpStringValue(request.url)),
|
||||
);
|
||||
state.network.delete(requestId);
|
||||
state.networkRequestTimestamps.delete(requestId);
|
||||
return;
|
||||
}
|
||||
const previous = state.network.get(requestId);
|
||||
const redirectResponse = recordValue(params.redirectResponse);
|
||||
if (
|
||||
Object.hasOwn(params, "redirectResponse") &&
|
||||
redirectResponse === undefined &&
|
||||
preserveMalformedRedirectEvidence(state, requestId, previous)
|
||||
)
|
||||
return;
|
||||
const redirects = [...(previous?.redirects ?? [])];
|
||||
if (redirectResponse !== undefined) {
|
||||
const rawResponseUrl = cdpStringValue(redirectResponse.url);
|
||||
const responseUrl =
|
||||
rawResponseUrl === undefined || rawResponseUrl === ""
|
||||
? undefined
|
||||
: allowedSanitizedUrl(rawResponseUrl, state.allowedOrigins);
|
||||
const responseUrlReason = redirectResponseUrlReason(rawResponseUrl);
|
||||
if (previous === undefined) {
|
||||
if (responseUrl === undefined)
|
||||
state.completeness.exclude("network_requests", responseUrlReason);
|
||||
else
|
||||
// CDP may deliver the continuation after its predecessor was excluded
|
||||
// by origin policy or before capture began. Keep the final request but
|
||||
// report missing prior coverage without calling it malformed protocol.
|
||||
state.completeness.attachLimited("network_requests");
|
||||
} else if (responseUrl === undefined) {
|
||||
// redirectResponse belongs to the same request ID and can contain an
|
||||
// excluded origin. Drop the entire chain before retaining any details.
|
||||
state.completeness.exclude("network_requests", responseUrlReason);
|
||||
state.network.delete(requestId);
|
||||
state.networkRequestTimestamps.delete(requestId);
|
||||
return;
|
||||
} else {
|
||||
redirects.push({
|
||||
url: previous.url,
|
||||
response_url: responseUrl.url,
|
||||
method: previous.method,
|
||||
resource_type: previous.resource_type,
|
||||
status: numberValue(redirectResponse.status) ?? null,
|
||||
mime_type: cdpStringValue(redirectResponse.mimeType) ?? null,
|
||||
encoded_data_length: (() => {
|
||||
const length = numberValue(redirectResponse.encodedDataLength);
|
||||
return length === undefined ? null : Math.max(0, length);
|
||||
})(),
|
||||
request_timestamp:
|
||||
state.networkRequestTimestamps.get(requestId) ?? null,
|
||||
// CDP reports redirectResponse on the next requestWillBeSent event.
|
||||
// This is the event boundary, not a separately observed response time.
|
||||
redirect_event_timestamp: numberValue(params.timestamp) ?? null,
|
||||
});
|
||||
}
|
||||
}
|
||||
const initiator = recordValue(params.initiator);
|
||||
const initiatorFrame = initiatorLocation(initiator);
|
||||
const rawInitiatorUrl = cdpStringValue(initiatorFrame?.url);
|
||||
@@ -155,6 +213,7 @@ export const handleRequestWillBeSent = (
|
||||
status: null,
|
||||
mime_type: null,
|
||||
encoded_data_length: null,
|
||||
redirects,
|
||||
initiator: {
|
||||
type: cdpStringValue(initiator?.type) ?? "other",
|
||||
url: initiatorUrl?.url ?? null,
|
||||
@@ -163,6 +222,44 @@ export const handleRequestWillBeSent = (
|
||||
},
|
||||
body_shapes: requestBodyShape(state, request),
|
||||
});
|
||||
const timestamp = numberValue(params.timestamp);
|
||||
if (timestamp === undefined) state.networkRequestTimestamps.delete(requestId);
|
||||
else state.networkRequestTimestamps.set(requestId, timestamp);
|
||||
};
|
||||
|
||||
const preserveMalformedRedirectEvidence = (
|
||||
state: CdpCaptureEventsState,
|
||||
requestId: string,
|
||||
previous: NetworkState | undefined,
|
||||
): boolean => {
|
||||
state.completeness.exclude("network_requests", "invalid_protocol_value");
|
||||
if (previous === undefined) return false;
|
||||
state.malformedRedirectRequestIds.add(requestId);
|
||||
if (
|
||||
state.input.include_json_body_shapes &&
|
||||
isJsonMediaType(previous.mime_type) &&
|
||||
previous.body_shapes.response === null
|
||||
) {
|
||||
updateResponseBodyShape(state, requestId, null);
|
||||
state.completeness.exclude("json_body_shapes", "invalid_protocol_value");
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const redirectResponseUrlReason = (
|
||||
value: string | undefined,
|
||||
):
|
||||
| "invalid_protocol_value"
|
||||
| "disallowed_origin"
|
||||
| "unsupported_url"
|
||||
| "unattributed_origin" => {
|
||||
if (value === undefined || value === "") return "invalid_protocol_value";
|
||||
try {
|
||||
new URL(value);
|
||||
} catch {
|
||||
return "invalid_protocol_value";
|
||||
}
|
||||
return exclusionReasonForUrl(value);
|
||||
};
|
||||
|
||||
export const handleResponseReceived = (
|
||||
@@ -174,6 +271,7 @@ export const handleResponseReceived = (
|
||||
state.completeness.exclude("network_requests", "invalid_protocol_value");
|
||||
return;
|
||||
}
|
||||
if (state.malformedRedirectRequestIds.has(requestId)) return;
|
||||
const current = state.network.get(requestId);
|
||||
const response = recordValue(params.response);
|
||||
if (current === undefined) return;
|
||||
@@ -186,6 +284,7 @@ export const handleResponseReceived = (
|
||||
: exclusionReasonForUrl(cdpStringValue(response.url)),
|
||||
);
|
||||
state.network.delete(requestId);
|
||||
state.networkRequestTimestamps.delete(requestId);
|
||||
return;
|
||||
}
|
||||
state.network.set(requestId, {
|
||||
@@ -209,6 +308,7 @@ export const handleLoadingFinished = (
|
||||
): void => {
|
||||
const requestId = cdpStringValue(params.requestId);
|
||||
if (requestId === undefined) return;
|
||||
if (state.malformedRedirectRequestIds.has(requestId)) return;
|
||||
const current = state.network.get(requestId);
|
||||
if (current === undefined) return;
|
||||
state.network.set(requestId, {
|
||||
|
||||
@@ -11,6 +11,8 @@ export interface CdpCaptureEventsState {
|
||||
readonly scripts: Map<string, CapturedScript>;
|
||||
readonly executionContextFrames: Map<string, string>;
|
||||
readonly network: Map<string, NetworkState>;
|
||||
readonly networkRequestTimestamps: Map<string, number>;
|
||||
readonly malformedRedirectRequestIds: Set<string>;
|
||||
readonly allowedWebSockets: Set<string>;
|
||||
console: WebPageInspection["console"]["events"];
|
||||
websockets: WebPageInspection["network"]["websocket_events"];
|
||||
|
||||
@@ -18,6 +18,8 @@ export class CdpCaptureEvents implements CdpCaptureEventsState {
|
||||
readonly scripts = new Map<string, CapturedScript>();
|
||||
readonly executionContextFrames = new Map<string, string>();
|
||||
readonly network = new Map<string, NetworkState>();
|
||||
readonly networkRequestTimestamps = new Map<string, number>();
|
||||
readonly malformedRedirectRequestIds = new Set<string>();
|
||||
readonly allowedWebSockets = new Set<string>();
|
||||
console: WebPageInspection["console"]["events"] = [];
|
||||
websockets: WebPageInspection["network"]["websocket_events"] = [];
|
||||
@@ -51,6 +53,8 @@ export class CdpCaptureEvents implements CdpCaptureEventsState {
|
||||
this.scripts.clear();
|
||||
this.executionContextFrames.clear();
|
||||
this.network.clear();
|
||||
this.networkRequestTimestamps.clear();
|
||||
this.malformedRedirectRequestIds.clear();
|
||||
this.allowedWebSockets.clear();
|
||||
this.console.length = 0;
|
||||
this.websockets.length = 0;
|
||||
@@ -135,16 +139,21 @@ export class CdpCaptureEvents implements CdpCaptureEventsState {
|
||||
responseBodyRequestIds(): readonly string[] {
|
||||
if (!this.input.include_json_body_shapes) return [];
|
||||
return [...this.network.values()]
|
||||
.filter(
|
||||
(request) => !this.malformedRedirectRequestIds.has(request.request_id),
|
||||
)
|
||||
.filter((request) => isJsonMediaType(request.mime_type))
|
||||
.map((request) => request.request_id);
|
||||
}
|
||||
|
||||
responseBodyUnavailable(requestId: string): void {
|
||||
if (this.malformedRedirectRequestIds.has(requestId)) return;
|
||||
bodyShapes.updateResponseBodyShape(this, requestId, null);
|
||||
this.completeness.unavailable("json_body_shapes");
|
||||
}
|
||||
|
||||
ingestResponseBody(requestId: string, value: unknown): void {
|
||||
if (this.malformedRedirectRequestIds.has(requestId)) return;
|
||||
bodyShapes.ingestResponseBodyShape(this, requestId, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { browserScenarioSchema } from "../domain/browserScenario.js";
|
||||
|
||||
it("uses exact accessible-name matching without a caller confirmation field", async () => {
|
||||
const scenario = browserScenarioSchema.parse({
|
||||
browser: { mode: "launch", executable_path: "chrome" },
|
||||
browser: { mode: "launch", executable_path: "/opt/chromium/chrome" },
|
||||
start_url: { url: "https://app.example.test/" },
|
||||
actions: [
|
||||
{
|
||||
|
||||
@@ -18,7 +18,7 @@ import { projectInputIssues } from "../domain/inputIssueProjection.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import type { Logger } from "../logger.js";
|
||||
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
||||
import { parseCliJsonInput } from "../cliJsonInput.js";
|
||||
import { parseCliJsonInput, resolveCliJsonPaths } from "../cliJsonInput.js";
|
||||
import {
|
||||
electronPageInspectionOptions,
|
||||
javascriptApplicationOptions,
|
||||
@@ -57,7 +57,11 @@ const registerElectronActiveCommand = (
|
||||
);
|
||||
if (!input.ok) return input.error;
|
||||
const parsed = electronActiveObservationInputSchema.safeParse(
|
||||
input.value,
|
||||
resolveCliJsonPaths(input.value, [
|
||||
["executable_path"],
|
||||
["application_path"],
|
||||
["application_root"],
|
||||
]),
|
||||
);
|
||||
if (!parsed.success)
|
||||
return inputError(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { z } from "incur";
|
||||
import { resolve } from "node:path";
|
||||
import { FirmwareAnalysisService } from "../application/firmware/FirmwareAnalysisService.js";
|
||||
import { createFirmwareAnalysisProvider } from "../composition/firmware.js";
|
||||
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
||||
@@ -38,7 +39,8 @@ export const registerFirmwareCommands = (
|
||||
execute(
|
||||
CLI_COMMANDS.inspectFirmwareRegions,
|
||||
"inspect_firmware_regions",
|
||||
args,
|
||||
// CLI paths are operator-relative; resolve before shared validation.
|
||||
{ path: resolve(args.path) },
|
||||
),
|
||||
});
|
||||
cli.command(CLI_COMMANDS.extractFirmware, {
|
||||
@@ -81,8 +83,8 @@ export const registerFirmwareCommands = (
|
||||
}),
|
||||
run: ({ args, options }) =>
|
||||
execute(CLI_COMMANDS.extractFirmware, "extract_firmware", {
|
||||
path: args.path,
|
||||
output_directory: args.outputDirectory,
|
||||
path: resolve(args.path),
|
||||
output_directory: resolve(args.outputDirectory),
|
||||
...(options.offset === undefined && options.length === undefined
|
||||
? {}
|
||||
: { range: { offset: options.offset, length: options.length } }),
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
captureProcessScenarioFile,
|
||||
compareProcessEvidenceFiles,
|
||||
isProcessCliFailure,
|
||||
} from "../application/ProcessCli.js";
|
||||
} from "../application/process/ProcessCli.js";
|
||||
import { logCliCommand } from "../cliLogging.js";
|
||||
import type { Logger } from "../logger.js";
|
||||
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
||||
|
||||
@@ -4,7 +4,7 @@ import { z } from "zod";
|
||||
import { captureBrowserScenario } from "./application/BrowserScenarioCaptureService.js";
|
||||
import { createBrowserScenarioProvider } from "./composition/browserScenario.js";
|
||||
import { CLI_COMMANDS } from "./cliCommandNames.js";
|
||||
import { parseCliJsonInput } from "./cliJsonInput.js";
|
||||
import { parseCliJsonInput, resolveCliJsonPaths } from "./cliJsonInput.js";
|
||||
import { logCliCommand } from "./cliLogging.js";
|
||||
import { AnalysisInputError } from "./domain/analysisErrorCore.js";
|
||||
import { projectAnalysisError } from "./domain/analysisErrorProjection.js";
|
||||
@@ -32,7 +32,9 @@ export const registerBrowserScenarioCommands = (
|
||||
logCliCommand(logger, CLI_COMMANDS.captureBrowserScenario, async () => {
|
||||
const input = await parseCliJsonInput(args.inputJson, OPERATION);
|
||||
if (!input.ok) return input.error;
|
||||
const scenario = browserScenarioSchema.safeParse(input.value);
|
||||
const scenario = browserScenarioSchema.safeParse(
|
||||
resolveCliJsonPaths(input.value, [["browser", "executable_path"]]),
|
||||
);
|
||||
if (!scenario.success)
|
||||
return cliError(
|
||||
new AnalysisInputError(
|
||||
|
||||
@@ -6,11 +6,11 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import { createCli } from "./cli.js";
|
||||
import { createSystemDoctorHost } from "./doctorRuntime.js";
|
||||
import { captureProcessScenarioFile } from "./application/ProcessCli.js";
|
||||
import { captureProcessScenarioFile } from "./application/process/ProcessCli.js";
|
||||
import { runCapabilityStatus } from "./composition/directAnalysis.js";
|
||||
import { probeProcessCaptureCapability } from "./application/ProcessHarness.js";
|
||||
import { probeProcessCaptureCapability } from "./process/capture/ProcessHarness.js";
|
||||
import { parseEvidence } from "./domain/evidence.js";
|
||||
import { parseProcessCapture } from "./domain/processCapture.js";
|
||||
import { parseProcessCapture } from "./domain/process/processCapture.js";
|
||||
|
||||
describe("the CLI takes its environment as an input", () => {
|
||||
it("builds from an explicitly supplied environment", () => {
|
||||
|
||||
+38
-1
@@ -1,4 +1,5 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import { AnalysisInputError } from "./domain/analysisErrorCore.js";
|
||||
import { projectAnalysisError } from "./domain/analysisErrorProjection.js";
|
||||
@@ -33,11 +34,47 @@ export const parseCliJsonInput = async (
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve named string fields of CLI-supplied JSON against the operator
|
||||
* working directory. Each entry is a key path from the document root; only
|
||||
* fields holding strings are resolved, everything else passes through
|
||||
* untouched. Shared MCP contracts reject relative paths, so CLI workflows
|
||||
* resolve operator-relative values before validation.
|
||||
*/
|
||||
export const resolveCliJsonPaths = (
|
||||
value: unknown,
|
||||
paths: ReadonlyArray<ReadonlyArray<string>>,
|
||||
): unknown => {
|
||||
let resolved = value;
|
||||
for (const keys of paths) resolved = resolveJsonPath(resolved, keys);
|
||||
return resolved;
|
||||
};
|
||||
|
||||
const resolveJsonPath = (
|
||||
value: unknown,
|
||||
keys: ReadonlyArray<string>,
|
||||
): unknown => {
|
||||
const [head, ...tail] = keys;
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value))
|
||||
return value;
|
||||
const entries = Object.entries(value).map(
|
||||
([key, entry]: readonly [string, unknown]): readonly [string, unknown] => {
|
||||
if (key !== head) return [key, entry];
|
||||
if (tail.length === 0)
|
||||
return [key, typeof entry === "string" ? resolve(entry) : entry];
|
||||
return [key, resolveJsonPath(entry, tail)];
|
||||
},
|
||||
);
|
||||
return Object.fromEntries(entries);
|
||||
};
|
||||
|
||||
const cannotBeAnExistingFile = (cause: unknown): boolean =>
|
||||
typeof cause === "object" &&
|
||||
cause !== null &&
|
||||
"code" in cause &&
|
||||
(cause.code === "ENOENT" || cause.code === "ENAMETOOLONG");
|
||||
(cause.code === "ENOENT" ||
|
||||
cause.code === "ENAMETOOLONG" ||
|
||||
cause.code === "ENOTDIR");
|
||||
|
||||
const hasExplicitJsonFileExtension = (value: string): boolean =>
|
||||
value.toLowerCase().endsWith(".json");
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
type UninstallHost,
|
||||
} from "./application/Uninstall.js";
|
||||
import { CLI_COMMANDS } from "./cliCommandNames.js";
|
||||
import { isProcessCliFailure } from "./application/ProcessCli.js";
|
||||
import { isProcessCliFailure } from "./application/process/ProcessCli.js";
|
||||
|
||||
describe("CLI operation status classification", () => {
|
||||
it.each([
|
||||
|
||||
@@ -4,9 +4,9 @@ import { jsonValueSchema } from "../domain/jsonValue.js";
|
||||
import {
|
||||
compareProcessCaptures,
|
||||
parseProcessCapture,
|
||||
} from "../domain/processCapture.js";
|
||||
} from "../domain/process/processCapture.js";
|
||||
import { FUNCTION_COMPARISON_EXAMPLE } from "./functionComparisonExample.js";
|
||||
import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "./processCaptureExample.js";
|
||||
import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "./process/processCaptureExample.js";
|
||||
|
||||
const comparison = compareFunctions(
|
||||
FUNCTION_COMPARISON_EXAMPLE.left,
|
||||
|
||||
@@ -4,16 +4,21 @@ import type { ToolContract } from "../toolContracts.js";
|
||||
import { managedOutputSchemas } from "../toolOutputSchemas.js";
|
||||
import { toolContractMetadata } from "../toolEffects.js";
|
||||
import { requireOutputSchema } from "../toolOutputSchemaPrimitives.js";
|
||||
import { isAbsoluteLocalPath } from "../../domain/localPath.js";
|
||||
|
||||
/** Exact caller boundary for execution-free PE/CLI triage and identity. */
|
||||
const managedTargetInputSchema = z.object({
|
||||
const managedTargetInputSchema = z.strictObject({
|
||||
path: z
|
||||
.string()
|
||||
.min(1)
|
||||
.optional()
|
||||
.refine(isAbsoluteLocalPath, {
|
||||
message:
|
||||
"path must be an absolute local filesystem path (for example /tmp/Example.dll or C:\\Example.dll)",
|
||||
})
|
||||
.describe(
|
||||
"Explicit managed PE/CLI path; omit to reuse the previously selected managed target",
|
||||
),
|
||||
"Explicit absolute managed PE/CLI path; omit to reuse the previously selected managed target",
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const managedArtifactInputSchema = managedTargetInputSchema;
|
||||
|
||||
@@ -30,12 +30,13 @@ const native = <Name extends string, Schema extends z.ZodObject>(
|
||||
inputSchema: Schema,
|
||||
) => {
|
||||
const outputSchema = requireOutputSchema(nativeOutputSchemas, name);
|
||||
const strictInputSchema = inputSchema.strict();
|
||||
return {
|
||||
name,
|
||||
...toolContractMetadata(name),
|
||||
description,
|
||||
kind: "native-provider",
|
||||
inputSchema,
|
||||
inputSchema: strictInputSchema,
|
||||
outputSchema,
|
||||
examples: [
|
||||
{
|
||||
@@ -45,7 +46,7 @@ const native = <Name extends string, Schema extends z.ZodObject>(
|
||||
.parse(examples[name] ?? {}),
|
||||
},
|
||||
],
|
||||
} satisfies ToolContract<Name, Schema, typeof outputSchema>;
|
||||
} satisfies ToolContract<Name, typeof strictInputSchema, typeof outputSchema>;
|
||||
};
|
||||
|
||||
/** Ordered single-line symbols; `swift-demangle` ends each result with LF. */
|
||||
|
||||
@@ -24,16 +24,18 @@ const official = <Name extends string, Schema extends z.ZodObject>(
|
||||
name: Name,
|
||||
description: string,
|
||||
inputSchema: Schema,
|
||||
) =>
|
||||
({
|
||||
) => {
|
||||
const strictInputSchema = inputSchema.strict();
|
||||
return {
|
||||
name,
|
||||
...toolContractMetadata(name),
|
||||
description,
|
||||
kind: "official-proxy",
|
||||
inputSchema,
|
||||
inputSchema: strictInputSchema,
|
||||
outputSchema: requireOutputSchema(officialOutputSchemas, name),
|
||||
examples: examplesFor(name, inputSchema),
|
||||
}) satisfies ToolContract<Name, Schema>;
|
||||
examples: examplesFor(name, strictInputSchema),
|
||||
} satisfies ToolContract<Name, typeof strictInputSchema>;
|
||||
};
|
||||
|
||||
/** Bridge operations exposed without additional application composition. */
|
||||
export const OFFICIAL_TOOL_CONTRACTS = [
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
/** Minimal valid process capture retained as a public contract example. */
|
||||
export { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../../domain/process/processCaptureExample.js";
|
||||
@@ -1,2 +0,0 @@
|
||||
/** Minimal valid process capture retained as a public contract example. */
|
||||
export { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../domain/processCaptureExample.js";
|
||||
@@ -1,21 +1,46 @@
|
||||
import { executableFormatHintSchema } from "../domain/dosCom.js";
|
||||
import { isAbsoluteLocalPath } from "../domain/localPath.js";
|
||||
import { z } from "zod";
|
||||
import { analysisProviderSelectorSchema } from "./providerSelection.js";
|
||||
|
||||
/** Caller-supplied analysis snapshot path; must name its destination absolutely. */
|
||||
const snapshotPathSchema = z
|
||||
.string()
|
||||
.min(1)
|
||||
.refine(isAbsoluteLocalPath, {
|
||||
message:
|
||||
"snapshot_path must be an absolute local filesystem path (for example /tmp/rea/analysis.json or C:\\rea\\analysis.json)",
|
||||
})
|
||||
.describe(
|
||||
"Absolute local filesystem path for the analysis snapshot; relative paths are rejected.",
|
||||
);
|
||||
|
||||
/** Caller-supplied analysis target path; must name the target absolutely. */
|
||||
const binaryTargetPathSchema = z
|
||||
.string()
|
||||
.min(1)
|
||||
.refine(isAbsoluteLocalPath, {
|
||||
message:
|
||||
"path must be an absolute local filesystem path (for example /tmp/fixture.bin or C:\\analysis\\fixture.bin)",
|
||||
})
|
||||
.describe(
|
||||
"Absolute local filesystem path for the analysis target; relative paths are rejected.",
|
||||
);
|
||||
|
||||
/** Input contract for opening a target with an optional staged snapshot. */
|
||||
export const openBinaryInputSchema = z.object({
|
||||
path: z.string().min(1),
|
||||
export const openBinaryInputSchema = z.strictObject({
|
||||
path: binaryTargetPathSchema,
|
||||
format: executableFormatHintSchema
|
||||
.optional()
|
||||
.describe(
|
||||
"Explicit headerless DOS COM interpretation; omission preserves header-based detection",
|
||||
),
|
||||
provider_id: analysisProviderSelectorSchema.optional(),
|
||||
snapshot_path: z.string().min(1).optional(),
|
||||
snapshot_path: snapshotPathSchema.optional(),
|
||||
});
|
||||
|
||||
/** Input contract for closing a target after an optional atomic snapshot. */
|
||||
export const closeBinaryInputSchema = z.object({
|
||||
snapshot_path: z.string().min(1).optional(),
|
||||
export const closeBinaryInputSchema = z.strictObject({
|
||||
snapshot_path: snapshotPathSchema.optional(),
|
||||
overwrite: z.boolean().default(false),
|
||||
});
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { browserScenarioBrowserSchema } from "../domain/browserScenarioValues.js";
|
||||
import { firmwareInputSchemas } from "../domain/firmware/firmwareAnalysis.js";
|
||||
import { electronActiveObservationInputSchema } from "../domain/javascript/electronActiveObservation.js";
|
||||
import { managedArtifactInputSchema } from "./managed/managedToolContracts.js";
|
||||
import { exportEvidenceBundleInputSchema } from "./sessionToolContracts.js";
|
||||
import {
|
||||
closeBinaryInputSchema,
|
||||
openBinaryInputSchema,
|
||||
} from "./sessionLifecycleInputs.js";
|
||||
import { importEvidenceBundleInputSchema } from "./sessionToolSchemas.js";
|
||||
import { binarySessionInputSchema } from "./sessionStatusContract.js";
|
||||
|
||||
const openBinaryAbsolute = (snapshot_path: string) =>
|
||||
openBinaryInputSchema.safeParse({ path: "/tmp/fixture", snapshot_path });
|
||||
|
||||
describe("session snapshot path inputs require absolute local paths", () => {
|
||||
it("rejects a relative snapshot path when opening a binary", () => {
|
||||
expect(openBinaryAbsolute("relative/analysis.json").success).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects a parent-relative snapshot path when opening a binary", () => {
|
||||
expect(openBinaryAbsolute("../outside/analysis.json").success).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts an absolute snapshot path when opening a binary", () => {
|
||||
expect(openBinaryAbsolute("/tmp/rea/analysis.json").success).toBe(true);
|
||||
});
|
||||
|
||||
it("still accepts open_binary without a snapshot path", () => {
|
||||
expect(
|
||||
openBinaryInputSchema.safeParse({ path: "/tmp/fixture" }).success,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects a relative snapshot path when closing a binary", () => {
|
||||
expect(
|
||||
closeBinaryInputSchema.safeParse({ snapshot_path: "analysis.json" })
|
||||
.success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts an absolute snapshot path when closing a binary", () => {
|
||||
expect(
|
||||
closeBinaryInputSchema.safeParse({ snapshot_path: "/tmp/analysis.json" })
|
||||
.success,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("still accepts close_binary without a snapshot path", () => {
|
||||
expect(closeBinaryInputSchema.safeParse({}).success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("evidence bundle export path requires an absolute local path", () => {
|
||||
it("rejects a relative export path", () => {
|
||||
expect(
|
||||
exportEvidenceBundleInputSchema.safeParse({ path: "out/bundle.json" })
|
||||
.success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects a parent-relative export path", () => {
|
||||
expect(
|
||||
exportEvidenceBundleInputSchema.safeParse({
|
||||
path: "../../bundle.json",
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts an absolute export path and keeps overwrite semantics", () => {
|
||||
expect(
|
||||
exportEvidenceBundleInputSchema.safeParse({
|
||||
path: "/tmp/bundle.json",
|
||||
}),
|
||||
).toMatchObject({ success: true, data: { overwrite: false } });
|
||||
});
|
||||
});
|
||||
|
||||
describe("managed target path requires an absolute local path", () => {
|
||||
it("rejects a relative managed target path", () => {
|
||||
expect(
|
||||
managedArtifactInputSchema.safeParse({ path: "bin/app.dll" }).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts an absolute managed target path", () => {
|
||||
expect(
|
||||
managedArtifactInputSchema.safeParse({ path: "/tmp/app.dll" }).success,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("still accepts omitting the path to reuse the active target", () => {
|
||||
expect(managedArtifactInputSchema.safeParse({}).success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe.runIf(process.platform === "win32")(
|
||||
"windows absolute path forms on windows hosts",
|
||||
() => {
|
||||
it("accepts drive-letter backslash snapshot paths", () => {
|
||||
expect(
|
||||
closeBinaryInputSchema.safeParse({
|
||||
snapshot_path: "C:\\rea\\analysis.json",
|
||||
}).success,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts drive-letter forward-slash snapshot paths", () => {
|
||||
expect(
|
||||
closeBinaryInputSchema.safeParse({
|
||||
snapshot_path: "C:/rea/analysis.json",
|
||||
}).success,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts forward-slash export paths", () => {
|
||||
expect(
|
||||
exportEvidenceBundleInputSchema.safeParse({
|
||||
path: "C:/rea/bundle.json",
|
||||
}).success,
|
||||
).toBe(true);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
describe("open binary target path requires an absolute local path", () => {
|
||||
it("rejects a relative target path", () => {
|
||||
expect(
|
||||
openBinaryInputSchema.safeParse({ path: "fixtures/app.bin" }).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts an absolute target path", () => {
|
||||
expect(
|
||||
openBinaryInputSchema.safeParse({ path: "/tmp/fixture.bin" }).success,
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("evidence bundle import path requires an absolute local path", () => {
|
||||
it("rejects a relative bundle path", () => {
|
||||
expect(
|
||||
importEvidenceBundleInputSchema.safeParse({ path: "evidence.json" })
|
||||
.success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts an absolute bundle path", () => {
|
||||
expect(
|
||||
importEvidenceBundleInputSchema.safeParse({ path: "/tmp/evidence.json" })
|
||||
.success,
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("firmware input path requires an absolute local path", () => {
|
||||
it("rejects a relative firmware path when inspecting regions", () => {
|
||||
expect(
|
||||
firmwareInputSchemas.inspect_firmware_regions.safeParse({
|
||||
path: "firmware.bin",
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects a relative firmware path when extracting", () => {
|
||||
expect(
|
||||
firmwareInputSchemas.extract_firmware.safeParse({
|
||||
path: "firmware.bin",
|
||||
output_directory: "/tmp/firmware-output",
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts an absolute firmware path", () => {
|
||||
expect(
|
||||
firmwareInputSchemas.inspect_firmware_regions.safeParse({
|
||||
path: "/tmp/firmware.bin",
|
||||
}).success,
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("browser executable path requires an absolute local path", () => {
|
||||
it("rejects a bare executable name", () => {
|
||||
expect(
|
||||
browserScenarioBrowserSchema.safeParse({
|
||||
mode: "launch",
|
||||
executable_path: "chrome",
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts an absolute executable path", () => {
|
||||
expect(
|
||||
browserScenarioBrowserSchema.safeParse({
|
||||
mode: "launch",
|
||||
executable_path: "/opt/chromium/chrome",
|
||||
}).success,
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("electron observation paths require absolute local paths", () => {
|
||||
const valid = {
|
||||
executable_path: "/Applications/Electron.app/Contents/MacOS/Electron",
|
||||
application_path: "/tmp/electron-app/main.js",
|
||||
};
|
||||
|
||||
it("rejects a relative executable path", () => {
|
||||
expect(
|
||||
electronActiveObservationInputSchema.safeParse({
|
||||
...valid,
|
||||
executable_path: "Electron",
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects a relative application path", () => {
|
||||
expect(
|
||||
electronActiveObservationInputSchema.safeParse({
|
||||
...valid,
|
||||
application_path: "main.js",
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects a relative application root", () => {
|
||||
expect(
|
||||
electronActiveObservationInputSchema.safeParse({
|
||||
...valid,
|
||||
application_root: "app",
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts absolute observation paths", () => {
|
||||
expect(
|
||||
electronActiveObservationInputSchema.safeParse({
|
||||
...valid,
|
||||
application_root: "/tmp/electron-app",
|
||||
}).success,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("still accepts omitting the application root", () => {
|
||||
expect(electronActiveObservationInputSchema.safeParse(valid).success).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("expected server path requires an absolute local path", () => {
|
||||
it("rejects a relative server path", () => {
|
||||
expect(
|
||||
binarySessionInputSchema.safeParse({
|
||||
expected_server_path: "dist/main.js",
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts an absolute server path", () => {
|
||||
expect(
|
||||
binarySessionInputSchema.safeParse({
|
||||
expected_server_path: "/opt/rea/dist/main.js",
|
||||
}).success,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("still accepts omitting the server path", () => {
|
||||
expect(binarySessionInputSchema.safeParse({}).success).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,23 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { isAbsoluteLocalPath } from "../domain/localPath.js";
|
||||
|
||||
/** Optional expectations used to compare the live session with its caller. */
|
||||
export const binarySessionInputSchema = z.object({
|
||||
export const binarySessionInputSchema = z.strictObject({
|
||||
expected_package_version: z.string().min(1).optional(),
|
||||
expected_catalog_digest: z
|
||||
.string()
|
||||
.regex(/^[a-f0-9]{64}$/u)
|
||||
.optional(),
|
||||
expected_server_path: z.string().min(1).optional(),
|
||||
expected_server_path: z
|
||||
.string()
|
||||
.min(1)
|
||||
.refine(isAbsoluteLocalPath, {
|
||||
message:
|
||||
"expected_server_path must be an absolute local filesystem path (for example /opt/rea/dist/main.js)",
|
||||
})
|
||||
.describe(
|
||||
"Absolute local filesystem path expected for the running server; relative paths are rejected.",
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { isAbsoluteLocalPath } from "../domain/localPath.js";
|
||||
import {
|
||||
sessionOutputSchemas,
|
||||
requireOutputSchema,
|
||||
@@ -47,7 +48,16 @@ const session = <Name extends string, Schema extends z.ZodObject>(
|
||||
|
||||
/** Session-owned Evidence bundle export options. */
|
||||
export const exportEvidenceBundleInputSchema = z.strictObject({
|
||||
path: z.string().min(1),
|
||||
path: z
|
||||
.string()
|
||||
.min(1)
|
||||
.refine(isAbsoluteLocalPath, {
|
||||
message:
|
||||
"path must be an absolute local filesystem path (for example /tmp/rea/evidence.json or C:\\rea\\evidence.json)",
|
||||
})
|
||||
.describe(
|
||||
"Absolute local filesystem path for the exported evidence bundle; relative paths are rejected.",
|
||||
),
|
||||
overwrite: z.boolean().default(false),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { isAbsoluteLocalPath } from "../domain/localPath.js";
|
||||
|
||||
import { artifactComparisonInputSchema } from "../domain/artifactComparison.js";
|
||||
import { bundleComparisonInputSchema } from "../domain/bundleComparison.js";
|
||||
import { callPathInputSchema } from "../domain/callPath.js";
|
||||
import { changedBehaviorInputSchema } from "../domain/changedBehavior.js";
|
||||
import { functionComparisonInputSchema } from "../domain/functionComparison.js";
|
||||
import { jsonValueSchema } from "../domain/jsonValue.js";
|
||||
import { processScenarioSchema } from "../domain/processCapture.js";
|
||||
import { processTraceSpecificationSchema } from "../domain/processTraceComparison.js";
|
||||
import { processScenarioSchema } from "../domain/process/processCapture.js";
|
||||
import { processTraceSpecificationSchema } from "../domain/process/processTraceComparison.js";
|
||||
import { recordUnknownInputSchema } from "../domain/residualUnknown.js";
|
||||
import { reconstructionVerificationInputSchema } from "../domain/reconstructionVerification.js";
|
||||
import { staticRuntimeCorrelationInputSchema } from "../domain/staticRuntimeCorrelation.js";
|
||||
@@ -36,7 +38,16 @@ export const addressContextInputSchema = z.strictObject({
|
||||
|
||||
/** Session-owned Evidence bundle import options. */
|
||||
export const importEvidenceBundleInputSchema = z.strictObject({
|
||||
path: z.string().min(1),
|
||||
path: z
|
||||
.string()
|
||||
.min(1)
|
||||
.refine(isAbsoluteLocalPath, {
|
||||
message:
|
||||
"path must be an absolute local filesystem path (for example /tmp/evidence.json or C:\\rea\\evidence.json)",
|
||||
})
|
||||
.describe(
|
||||
"Absolute local filesystem path for the evidence bundle to import; relative paths are rejected.",
|
||||
),
|
||||
});
|
||||
|
||||
/** Evidence references for deterministic process comparison. */
|
||||
|
||||
@@ -55,7 +55,7 @@ export const TOOL_EXAMPLE_OVERRIDES: Readonly<
|
||||
open_binary: { path: "/tmp/fixture" },
|
||||
export_evidence_bundle: { path: "/tmp/evidence.json" },
|
||||
inspect_address_context: { address: "0x1000" },
|
||||
import_evidence_bundle: { path: "evidence.json" },
|
||||
import_evidence_bundle: { path: "/tmp/evidence.json" },
|
||||
capture_process_scenario: {
|
||||
executable: "/usr/bin/true",
|
||||
working_directory: "/tmp",
|
||||
|
||||
@@ -15,7 +15,7 @@ import { evidenceBundleSchema } from "../domain/evidenceBundle.js";
|
||||
import {
|
||||
processCaptureComparisonSchema,
|
||||
processCaptureSchema,
|
||||
} from "../domain/processCapture.js";
|
||||
} from "../domain/process/processCapture.js";
|
||||
import {
|
||||
functionInstructionWindowSchema,
|
||||
referenceKindSchema,
|
||||
|
||||
@@ -94,7 +94,7 @@ const browserInput = {
|
||||
};
|
||||
|
||||
/** Public input for complete discovery, optionally filtered by exact origin. */
|
||||
export const listBrowserTargetsInputSchema = z.object({
|
||||
export const listBrowserTargetsInputSchema = z.strictObject({
|
||||
...browserInput,
|
||||
});
|
||||
|
||||
|
||||
@@ -114,6 +114,21 @@ const browserNetworkRequestSchema = z.object({
|
||||
status: z.number().nullable(),
|
||||
mime_type: z.string().nullable(),
|
||||
encoded_data_length: z.number().min(0).nullable(),
|
||||
redirects: z
|
||||
.array(
|
||||
z.object({
|
||||
url: z.string(),
|
||||
response_url: z.string(),
|
||||
method: z.string(),
|
||||
resource_type: z.string().nullable(),
|
||||
status: z.number().nullable(),
|
||||
mime_type: z.string().nullable(),
|
||||
encoded_data_length: z.number().min(0).nullable(),
|
||||
request_timestamp: z.number().nullable(),
|
||||
redirect_event_timestamp: z.number().nullable(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
initiator: z.object({
|
||||
type: z.string(),
|
||||
url: z.string().nullable(),
|
||||
|
||||
@@ -56,7 +56,7 @@ it("does not require duplicate origin scope declarations", () => {
|
||||
const scenario = browserScenarioSchema.parse({
|
||||
browser: {
|
||||
mode: "launch",
|
||||
executable_path: "chrome",
|
||||
executable_path: "/opt/chromium/chrome",
|
||||
},
|
||||
start_url: { url: "https://app.example.test/" },
|
||||
actions: [
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { isAbsoluteLocalPath } from "./localPath.js";
|
||||
import { browserNetworkContentSelectionSchema } from "./browserNetworkEvidence.js";
|
||||
|
||||
import {
|
||||
@@ -11,7 +12,14 @@ export const scenarioIdentifierSchema = z
|
||||
.string()
|
||||
.regex(/^[A-Za-z][A-Za-z0-9._-]*$/u);
|
||||
|
||||
const browserExecutablePathSchema = z.string().trim().min(1);
|
||||
const browserExecutablePathSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.refine(isAbsoluteLocalPath, {
|
||||
message:
|
||||
"executable_path must be an absolute local filesystem path (for example /opt/chromium/chrome or C:\\chromium\\chrome.exe)",
|
||||
});
|
||||
|
||||
const browserScenarioBaseUrlSchema = z
|
||||
.string()
|
||||
@@ -67,7 +75,9 @@ export type BrowserScenarioUrl = z.infer<typeof browserScenarioUrlSchema>;
|
||||
export const browserScenarioBrowserSchema = z.discriminatedUnion("mode", [
|
||||
z.strictObject({
|
||||
mode: z.literal("launch"),
|
||||
executable_path: browserExecutablePathSchema,
|
||||
executable_path: browserExecutablePathSchema.describe(
|
||||
"Absolute local filesystem path for the browser executable; relative paths are rejected.",
|
||||
),
|
||||
headless: z
|
||||
.boolean()
|
||||
.default(true)
|
||||
|
||||
@@ -24,7 +24,7 @@ export const parseCallPathAddress = (input: unknown): string =>
|
||||
inputAddressSchema.parse(input);
|
||||
|
||||
/** Strict input for explicit call-path reconstruction from complete dossiers. */
|
||||
export const callPathInputSchema = z.object({
|
||||
export const callPathInputSchema = z.strictObject({
|
||||
functions: z.array(evidenceSchema).min(1),
|
||||
start: z.object({ address: inputAddressSchema }).strict(),
|
||||
goal: z.object({ address: inputAddressSchema }).strict(),
|
||||
|
||||
@@ -62,6 +62,13 @@ describe("incremental canonical digest", () => {
|
||||
expect(digestCanonicalValue(value)).toBe(legacyDigest(value));
|
||||
});
|
||||
|
||||
it("keeps digests identical when buffered parts cross flush boundaries", () => {
|
||||
const manyKeys: Record<string, unknown> = {};
|
||||
for (let i = 0; i < 2000; i += 1) manyKeys[`k${i}`] = i;
|
||||
const value = { keys: manyKeys, tail: ["€", "😀", "x".repeat(9000)] };
|
||||
expect(digestCanonicalValue(value)).toBe(legacyDigest(value));
|
||||
});
|
||||
|
||||
it("preserves canonicalize property access and toJSON cycle behavior", () => {
|
||||
const createAccessor = () => {
|
||||
let reads = 0;
|
||||
|
||||
@@ -6,8 +6,19 @@ export const digestCanonicalValue = (
|
||||
context = "Comparison",
|
||||
): string => {
|
||||
const hash = createHash("sha256");
|
||||
const encoded = emitCanonical(value, (part) => hash.update(part), new Set());
|
||||
// Batch small emitted parts to cut hash.update crossings.
|
||||
// Flush cuts only at part boundaries, so the hashed byte stream is unchanged.
|
||||
let buffered = "";
|
||||
const emit = (part: string): void => {
|
||||
buffered += part;
|
||||
if (buffered.length >= 8192) {
|
||||
hash.update(buffered);
|
||||
buffered = "";
|
||||
}
|
||||
};
|
||||
const encoded = emitCanonical(value, emit, new Set());
|
||||
if (!encoded) throw new TypeError(`${context} could not canonicalize data`);
|
||||
if (buffered) hash.update(buffered);
|
||||
return hash.digest("hex");
|
||||
};
|
||||
|
||||
|
||||
@@ -7,19 +7,19 @@ import { functionComparisonResultSchema } from "./functionComparison.js";
|
||||
import {
|
||||
PROCESS_PROVIDER,
|
||||
isProcessEvidenceProvider,
|
||||
} from "./processEvidenceProvider.js";
|
||||
} from "./process/processEvidenceProvider.js";
|
||||
import {
|
||||
comparisonStatusSchema,
|
||||
deriveProcessComparisonStatus,
|
||||
PROCESS_COMPARISON_DIMENSIONS,
|
||||
processCaptureComparisonSchema,
|
||||
} from "./processCapture.js";
|
||||
} from "./process/processCapture.js";
|
||||
import { prefixedDigestSchema } from "./../domain/digests.js";
|
||||
|
||||
const evidenceIdSchema = prefixedDigestSchema("ev");
|
||||
|
||||
/** Input for aggregating existing comparison Evidence. */
|
||||
export const changedBehaviorInputSchema = z.object({
|
||||
export const changedBehaviorInputSchema = z.strictObject({
|
||||
comparisons: z.array(evidenceSchema).default([]),
|
||||
});
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ const makeUnknown = (
|
||||
dependency?: string,
|
||||
) => {
|
||||
const input = recordUnknownInputSchema.parse({
|
||||
approved: true,
|
||||
question,
|
||||
severity: "medium",
|
||||
domain: "bundle-projection",
|
||||
@@ -95,7 +94,6 @@ describe("evidenceBundleForTarget", () => {
|
||||
const unknowns = [foreignRoot.unknown, middle.unknown];
|
||||
for (let index = 0; index < 125_000; index += 1) {
|
||||
const input = recordUnknownInputSchema.parse({
|
||||
approved: true,
|
||||
question: `Target child ${index}`,
|
||||
severity: "medium",
|
||||
domain: "bundle-projection",
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
import { z } from "zod";
|
||||
import { digestSchema } from "../digests.js";
|
||||
import { jsonValueSchema } from "../jsonValue.js";
|
||||
import { isAbsoluteLocalPath } from "../localPath.js";
|
||||
|
||||
const bytes = z.number().int().min(0).max(Number.MAX_SAFE_INTEGER);
|
||||
const range = z.strictObject({ offset: bytes, length: bytes.positive() });
|
||||
const target = {
|
||||
path: z.string().min(1).describe("Local firmware file; never executed"),
|
||||
path: z
|
||||
.string()
|
||||
.min(1)
|
||||
.refine(isAbsoluteLocalPath, {
|
||||
message:
|
||||
"path must be an absolute local filesystem path (for example /tmp/firmware.bin or C:\\firmware\\firmware.bin)",
|
||||
})
|
||||
.describe(
|
||||
"Absolute local firmware file; never executed. Relative paths are rejected.",
|
||||
),
|
||||
};
|
||||
|
||||
/** Explicit firmware inspection and extraction intent, independent of engines. */
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
import { z } from "zod";
|
||||
|
||||
import { projectInputIssues } from "./inputIssueProjection.js";
|
||||
import { processScenarioSchema } from "./processScenario.js";
|
||||
import { processScenarioSchema } from "./process/processScenario.js";
|
||||
|
||||
describe("input issue projection", () => {
|
||||
it("preserves static regex guidance without echoing the rejected value", () => {
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const pathInputSchema = z.string().trim().min(1);
|
||||
import { isAbsoluteLocalPath } from "../localPath.js";
|
||||
|
||||
const pathInputSchema = z.string().trim().min(1).refine(isAbsoluteLocalPath, {
|
||||
message:
|
||||
"Electron executable, application, and root paths must be absolute local filesystem paths (for example /Applications/Electron.app/Contents/MacOS/Electron)",
|
||||
});
|
||||
const absolutePathSchema = z
|
||||
.string()
|
||||
.min(1)
|
||||
@@ -60,9 +65,17 @@ const actionSchema = z.discriminatedUnion("kind", [
|
||||
|
||||
/** Input for one explicit, provider-owned Electron runtime experiment. */
|
||||
export const electronActiveObservationInputSchema = z.strictObject({
|
||||
executable_path: pathInputSchema,
|
||||
application_path: pathInputSchema,
|
||||
application_root: pathInputSchema.optional(),
|
||||
executable_path: pathInputSchema.describe(
|
||||
"Absolute local filesystem path for the Electron executable; relative paths are rejected.",
|
||||
),
|
||||
application_path: pathInputSchema.describe(
|
||||
"Absolute local filesystem path for the Electron application; relative paths are rejected.",
|
||||
),
|
||||
application_root: pathInputSchema
|
||||
.optional()
|
||||
.describe(
|
||||
"Absolute local filesystem root for application-relative paths; omit to derive it from the application path. Relative paths are rejected.",
|
||||
),
|
||||
args: z.array(z.string()).default([]),
|
||||
actions: z.array(actionSchema).default([]),
|
||||
});
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { isAbsolute, relative, sep } from "node:path";
|
||||
|
||||
/** True when the value is an absolute filesystem path on the host platform. */
|
||||
export const isAbsoluteLocalPath = (value: string): boolean =>
|
||||
isAbsolute(value);
|
||||
|
||||
/** Test lexical containment on the host platform; callers must resolve symlinks first. */
|
||||
export const isPathWithinRoot = (root: string, path: string): boolean => {
|
||||
const value = relative(root, path);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { jsonValueSchema } from "./jsonValue.js";
|
||||
import { jsonValueSchema } from "../jsonValue.js";
|
||||
|
||||
import { normalizationSchema } from "./processScenario.js";
|
||||
import { collectProcessCaptureIssues } from "./processCaptureValidation.js";
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { JsonValue } from "./jsonValue.js";
|
||||
import type { JsonValue } from "../jsonValue.js";
|
||||
import { digestProcessCommitment } from "./processCapture.js";
|
||||
|
||||
const normalization = {
|
||||
@@ -2,7 +2,7 @@ import canonicalize from "canonicalize";
|
||||
import { z } from "zod";
|
||||
|
||||
import type { ProcessCapture } from "./processCapture.js";
|
||||
import { jsonValueSchema } from "./jsonValue.js";
|
||||
import { jsonValueSchema } from "../jsonValue.js";
|
||||
import {
|
||||
compareProcessTraces,
|
||||
processTraceOutcomesDiffer,
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import type { EvidenceProvider } from "./evidence.js";
|
||||
import type { EvidenceProvider } from "../evidence.js";
|
||||
|
||||
/** Canonical provider identity for controlled process capture Evidence. */
|
||||
export const PROCESS_PROVIDER = {
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { jsonValueSchema, type JsonValue } from "./jsonValue.js";
|
||||
import { jsonValueSchema, type JsonValue } from "../jsonValue.js";
|
||||
import type { UnverifiedProcessCapture } from "./processCapture.js";
|
||||
|
||||
/** Process-capture observation families shared by live drivers and trace assertions. */
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import type { JsonValue } from "./jsonValue.js";
|
||||
import type { JsonValue } from "../jsonValue.js";
|
||||
import type { ProcessCapture } from "./processCapture.js";
|
||||
import {
|
||||
processObservationLocationSchema,
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user